From debbugs-submit-bounces@debbugs.gnu.org Fri Jun 17 17:46:52 2022 Received: (at submit) by debbugs.gnu.org; 17 Jun 2022 21:46:52 +0000 Received: from localhost ([127.0.0.1]:47127 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o2Jno-00023a-ME for submit@debbugs.gnu.org; Fri, 17 Jun 2022 17:46:52 -0400 Received: from lists.gnu.org ([209.51.188.17]:51032) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o2Jnn-00023T-DK for submit@debbugs.gnu.org; Fri, 17 Jun 2022 17:46:51 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]:44682) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1o2Jnn-0004BL-2g for guix-patches@gnu.org; Fri, 17 Jun 2022 17:46:51 -0400 Received: from mx1.dismail.de ([78.46.223.134]:19624) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1o2Jni-0001bn-9e for guix-patches@gnu.org; Fri, 17 Jun 2022 17:46:50 -0400 Received: from mx1.dismail.de (localhost [127.0.0.1]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 1163a9ce for ; Fri, 17 Jun 2022 23:46:41 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; d=dismail.de; h=from:to:cc :subject:date:message-id:mime-version:content-type :content-transfer-encoding; s=20190914; bh=SQgq7o2pu02VqUs/ao+T2 MjVUMMZsDKuMzICIDcHnhQ=; b=DSot/WU1fp8mTLSQ3LD+vZVAEcesdxK8MVe76 SMkawGBl75EzATp+3trxWrgJNfxqbh7UZQ4sQAH9DSHU2NoPXVJ51N47wsDgQJAl Mi7jYMKu0xkgM4P89HCKDWxqActhtCzILfbnxyFGfn2zTGcozcOuh4XEv5JMbq95 95q5oKI0HrDsyT3vn2ZMOcFL3YosxltlVjFcKkfjdsoBG7ooMkXFQZFpF+EXid5J IifDb74DxY4MB8d1WrIPEhJmXrr6zaA3/nNpjUl6BgwecpkwZwXURa4tljtI7+w7 eubVhYxPnJ5zc2EmChwhSyabwdH3bOakftZgSFV7f6x7f0kKA== Received: from smtp1.dismail.de ( [10.240.26.11]) by mx1.dismail.de (OpenSMTPD) with ESMTP id e4448c81 for ; Fri, 17 Jun 2022 23:46:40 +0200 (CEST) Received: from smtp1.dismail.de (localhost [127.0.0.1]) by smtp1.dismail.de (OpenSMTPD) with ESMTP id 719b5bf9 for ; Fri, 17 Jun 2022 23:46:40 +0200 (CEST) Received: by dismail.de (OpenSMTPD) with ESMTPSA id ec7c0fd7 (TLSv1.3:AEAD-AES256-GCM-SHA384:256:NO); Fri, 17 Jun 2022 23:46:38 +0200 (CEST) From: Joshua Branson To: guix-patches@gnu.org Subject: [PATCH] services: mail: add opensmtpd records to enhance opensmtpd-configuration. Date: Fri, 17 Jun 2022 17:46:18 -0400 Message-Id: <20220617214618.12377-1-jbranso@dismail.de> X-Mailer: git-send-email 2.36.1 MIME-Version: 1.0 Content-Type: text/plain; charset=y Content-Transfer-Encoding: 8bit Received-SPF: pass client-ip=78.46.223.134; envelope-from=jbranso@dismail.de; helo=mx1.dismail.de X-Spam_score_int: -3 X-Spam_score: -0.4 X-Spam_bar: / X-Spam_report: (-0.4 / 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, MIME_CHARSET_FARAWAY=2.45, RCVD_IN_DNSWL_LOW=-0.7, 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-Debbugs-Envelope-To: submit Cc: Joshua Branson X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" Openmstpd-configuration may only be configured by a config-file. This patch, enables one to configure opensmtpd by using some guile record types (defined via define-record-type*). This patch is mostly complete, but I could use some guidance on what else needs to be done for it to be accepted in to guix properly. I do have some documentation written for the opensmtpd-service, but it is probably not complete and is currently written in the org-mode format. * gnu/services/mail.scm (opensmtpd-table-configuration): New record. * gnu/services/mail.scm (opensmtpd-ca-configuration): New record. * gnu/services/mail.scm (opensmtpd-pki-configuration): New record. * gnu/services/mail.scm (opensmtpd-action-local-delivery-configuration): New record. * gnu/services/mail.scm (opensmtpd-maildir-configuration): New record. * gnu/services/mail.scm (opensmtpd-mda-configuration): New record. * gnu/services/mail.scm (opensmtpd-action-relay-configuration): New record. * gnu/services/mail.scm (opensmtpd-option-configuration): New record. * gnu/services/mail.scm (opensmtpd-filter-phase-configuration): New record. * gnu/services/mail.scm (opensmtpd-filter-configuration): New record. * gnu/services/mail.scm (opensmtpd-listen-on-configuration): New record. * gnu/services/mail.scm (opensmtpd-listen-on-socket-configuration): New record. * gnu/services/mail.scm (opensmtpd-match-configuration): New record. * gnu/services/mail.scm (opensmtpd-smtp-configuration): New record. * gnu/services/mail.scm (opensmtpd-srs-configuration): New record. * gnu/services/mail.scm (opensmtpd-queue-configuration): New record. * gnu/services/mail.scm (opensmtpd-configuration): New record. --- gnu/services/mail.scm | 2016 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 2013 insertions(+), 3 deletions(-) diff --git a/gnu/services/mail.scm b/gnu/services/mail.scm index d99743ac31..bdc0ee3bf7 100644 --- a/gnu/services/mail.scm +++ b/gnu/services/mail.scm @@ -57,8 +57,143 @@ (define-module (gnu services mail) mailbox-configuration namespace-configuration + opensmtpd-table-configuration + opensmtpd-table-configuration? + opensmtpd-table-configuration-name + opensmtpd-table-configuration-file-db + opensmtpd-table-configuration-data + + opensmtpd-ca-configuration + opensmtpd-ca-configuration? + opensmtpd-ca-configuration-name + opensmtpd-ca-configuration-file + + opensmtpd-pki-configuration + opensmtpd-pki-configuration? + opensmtpd-pki-configuration-domain + opensmtpd-pki-configuration-cert + opensmtpd-pki-configuration-key + opensmtpd-pki-configuration-dhe + + opensmtpd-action-local-delivery-configuration + opensmtpd-action-local-delivery-configuration? + opensmtpd-action-local-delivery-configuration-method + opensmtpd-action-local-delivery-configuration-alias + opensmtpd-action-local-delivery-configuration-ttl + opensmtpd-action-local-delivery-configuration-user + opensmtpd-action-local-delivery-configuration-userbase + opensmtpd-action-local-delivery-configuration-virtual + opensmtpd-action-local-delivery-configuration-wrapper + + opensmtpd-maildir-configuration + opensmtpd-maildir-configuration? + opensmtpd-maildir-configuration-pathname + opensmtpd-maildir-configuration-junk + + opensmtpd-mda-configuration + opensmtpd-mda-configuration-name + opensmtpd-mda-configuration-command + + opensmtpd-action-relay-configuration + opensmtpd-action-relay-configuration? + opensmtpd-action-relay-configuration-backup + opensmtpd-action-relay-configuration-backup-mx + opensmtpd-action-relay-configuration-helo + opensmtpd-action-relay-configuration-domain + opensmtpd-action-relay-configuration-host + opensmtpd-action-relay-configuration-pki + opensmtpd-action-relay-configuration-srs + opensmtpd-action-relay-configuration-tls + opensmtpd-action-relay-configuration-auth + opensmtpd-action-relay-configuration-mail-from + opensmtpd-action-relay-configuration-src + + opensmtpd-option-configuration + opensmtpd-option-configuration? + opensmtpd-option-configuration-option + opensmtpd-option-configuration-not + opensmtpd-option-configuration-regex + opensmtpd-option-configuration-data + + opensmtpd-filter-phase-configuration + opensmtpd-filter-phase-configuration? + opensmtpd-filter-phase-configuration-name + opensmtpd-filter-phase-configuration-phase-name + opensmtpd-filter-phase-configuration-options + opensmtpd-filter-phase-configuration-decision + opensmtpd-filter-phase-configuration-message + opensmtpd-filter-phase-configuration-value + + opensmtpd-filter-configuration + opensmtpd-filter-configuration? + opensmtpd-filter-configuration-name + opensmtpd-filter-configuration-proc + + opensmtpd-listen-on-configuration + opensmtpd-listen-on-configuration? + opensmtpd-listen-on-configuration-interface + opensmtpd-listen-on-configuration-family + opensmtpd-listen-on-configuration-auth + opensmtpd-listen-on-configuration-auth-optional + opensmtpd-listen-on-configuration-filters + opensmtpd-listen-on-configuration-hostname + opensmtpd-listen-on-configuration-hostnames + opensmtpd-listen-on-configuration-mask-src + opensmtpd-listen-on-configuration-disable-dsn + opensmtpd-listen-on-configuration-pki + opensmtpd-listen-on-configuration-port + opensmtpd-listen-on-configuration-proxy-v2 + opensmtpd-listen-on-configuration-received-auth + opensmtpd-listen-on-configuration-senders + opensmtpd-listen-on-configuration-secure-connection + opensmtpd-listen-on-configuration-tag + + opensmtpd-listen-on-socket-configuration + opensmtpd-listen-on-socket-configuration? + opensmtpd-listen-on-socket-configuration-filters + opensmtpd-listen-on-socket-configuration-mask-src + opensmtpd-listen-on-socket-configuration-tag + + opensmtpd-match-configuration + opensmtpd-match-configuration? + opensmtpd-match-configuration-action + opensmtpd-match-configuration-options + + opensmtpd-smtp-configuration + opensmtpd-smtp-configuration? + opensmtpd-smtp-configuration-ciphers + opensmtpd-smtp-configuration-limit-max-mails + opensmtpd-smtp-configuration-limit-max-rcpt + opensmtpd-smtp-configuration-max-message-size + opensmtpd-smtp-configuration-sub-addr-delim character + + opensmtpd-srs-configuration + opensmtpd-srs-configuration? + opensmtpd-srs-configuration-key + opensmtpd-srs-configuration-backup-key + opensmtpd-srs-configuration-ttl-delay + + opensmtpd-queue-configuration + opensmtpd-queue-configuration? + opensmtpd-queue-configuration-compression + opensmtpd-queue-configuration-encryption + opensmtpd-queue-configuration-ttl-delay + opensmtpd-configuration opensmtpd-configuration? + opensmtpd-package + opensmtpd-config-file + opensmtpd-configuration-bounce + opensmtpd-configuration-listen-ons + opensmtpd-configuration-listen-on-socket + opensmtpd-configuration-includes + opensmtpd-configuration-matches + opensmtpd-configuration-mda-wrappers + opensmtpd-configuration-mta-max-deferred + opensmtpd-configuration-srs + opensmtpd-configuration-smtp + opensmtpd-configuration-queue + opensmtpd-service-type %default-opensmtpd-config-file @@ -1651,13 +1786,1888 @@ (define (generate-dovecot-documentation) ;;; OpenSMTPD. ;;; +;; some fieldnames have a default value of #f, which is ok. They cannot have a value of #t. +;; for example opensmtpd-table-configuration-data can be #f, BUT NOT true. +;; my/sanitize procedure tests values to see if they are of the right kind. +;; procedure false? is needed to allow fields like 'values' to be blank, (empty), or #f BUT also +;; have a value like a list of strings. +(define (false? var) + (eq? #f var)) + +;; this procedure takes in a var and a list of procedures. It loops through list of procedures passing in var to each. +;; if one procedure returns #t, the function returns true. Otherwise #f. +;; TODO for fun rewrite this using map +;; If I rewrote it in map, then it may help with sanitizing. +;; eg: I could then potentially easily sanitize vars with lambda procedures. +(define (is-value-right-type? var list-of-procedures record fieldname) + (if (null? list-of-procedures) + #f + (cond [(procedure? (car list-of-procedures)) + (if ((car list-of-procedures) var) + #t + (is-value-right-type? var (cdr list-of-procedures) record fieldname))] + [(and (sanitize-configuration? (car list-of-procedures)) + (sanitize-configuration-error-if-proc-fails (car list-of-procedures)) + (if ((sanitize-configuration-proc (car list-of-procedures)) var) + #t + (begin + (apply string-append + (sanitize-configuration-error-message (car list-of-procedures))) + (throw 'bad! var))))] + [else (if ((sanitize-configuration-proc (car list-of-procedures)) var) + #t + (is-value-right-type? var (cdr list-of-procedures) record fieldname))]))) + +;; converts strings like this: +;; "apple, ham, cherry" -> "apple, ham, or cherry" +;; "pineapple" -> "pinneapple". +;; "cheese, grapefruit, or jam" -> "cheese, grapefruit, or jam" +(define (add-comma-or string) + (define last-comma-location (string-rindex string #\,)) + (if last-comma-location + (if (string-contains string ", or" last-comma-location) + string + (string-replace string ", or" last-comma-location + (+ 1 last-comma-location))) + string)) + +;; I could test for read-ability of a file, but then I would have to +;; test the program as root everytime instead of as a normal user... +(define (file-exists? file) +(if (string? file) + (access? file F_OK) + #f)) + +(define (list-of-procedures->string procedures) + (define string + (let loop ([procedures procedures]) + (if (null? procedures) + "" + (begin + (string-append + (cond [(eq? false? (car procedures)) + "#f , "] + [(eq? boolean? (car procedures)) + "boolean, "] + [(eq? string? (car procedures)) + "string, "] + [(eq? integer? (car procedures)) + "integer, "] + [(eq? list-of-strings? (car procedures)) + "list of strings, "] + [(eq? assoc-list? (car procedures)) + "an association list, "] + [(eq? opensmtpd-pki-configuration? (car procedures)) + "an record, "] + [(eq? opensmtpd-table-configuration? (car procedures)) + "an record, "] + [(eq? list-of-unique-opensmtpd-match-configuration? (car procedures)) + "a list of unique records, "] + [(eq? table-whose-data-are-assoc-list? (car procedures)) + (string-append + "an record whose fieldname 'values' are an assoc-list \n" + "(eg: (opensmtpd-table-configuration (name \"table\") (data '(\"joshua\" . \"$encrypted$password\")))), ")] + [(eq? file-exists? (car procedures)) + "file, "] + [else "has an incorrect value, "]) + (loop (cdr procedures))))))) + (add-comma-or (string-append (string-drop-right string 2) ".\n"))) + +;; TODO can I M-x raise-sexp (string=? string var) in this procedure? and get rid of checking +;; if the var is a string? The previous string-in-list? had that check. +;; (string-in-list? '("hello" 5 "cat")) currently works. If I M-x raise-sexp (string=? string var) +;; then it will no longer work. +(define (string-in-list? string list) + (primitive-eval (cons 'or (map (lambda (var) (and (string? var) (string=? string var))) list)))) + +(define (my/sanitize var record fieldname list-of-procedures) + (if (is-value-right-type? var list-of-procedures record fieldname) + var + (begin + (display (string-append "<" record "> fieldname: '" fieldname "' is of type " + (list-of-procedures->string list-of-procedures) "\n")) + (throw 'bad! var)))) + +;; Some example opensmtpd-table-configurations: +;; +;; (opensmtpd-table-configuration (name "root accounts") (data '(("joshua" . "root@dismail.de") ("joshua" . "postmaster@dismail.de")))) +;; (opensmtpd-table-configuration (name "root accounts") (data (list "mysite.me" "your-site.com"))) +;; TODO should support have a fieldname 'file'? +;; Or should I change name to name-or-file ? +(define-record-type* + opensmtpd-table-configuration make-opensmtpd-table-configuration + opensmtpd-table-configuration? + this-record + (name opensmtpd-table-configuration-name ;; string + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-table-configuration" "name" (list string?))))) + (file-db opensmtpd-table-configuration-file-db + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-table-configuration" "file-db" + (list boolean?))))) + ;; FIXME support an aliasing table as described here: + ;; https://man.openbsd.org/table.5 + ;; One may have to use the record file for this. I don't think tables support a table like this: + ;; table "name" { joshua = joshua@gnucode.me,joshua@gnu-hurd.com,joshua@propernaming.org, root = root@gnucode.me } + ;; If values is an absolute filename, then it will use said filename to house the table info. + ;; filename must be an absolute filename. + (data opensmtpd-table-configuration-data + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-table-configuration" "values" + (list file-exists? list-of-strings? assoc-list?))))) + ;; is a list of values or key values + ;; eg: (list "mysite.me" "your-site.com") + ;; eg: (list ("joshua" . "joshua@gnu.org") ("james" . "james@gnu.org")) + ;; I am currently making these values be as assocation list of strings only. + ;; FIXME should I allow a var like this? + ;; (list (cons "gnucode.me" 234.949.392.23)) + ;; can be of type: (quote list-of-strings) or (quote assoc-list) + ;; (opensmtpd-table-configuration-type record) returns the values' type. The user SHOULD NEVER set the type. + ;; TODO jpoiret: on irc reccomends that I just use an outside function to determine fieldname 'values', type. + ;; it would be "simpler" and possibly easier for the next person working on this code to understand what is happening. + (type opensmtpd-table-configuration-type + (default #f) + (thunked) + (sanitize (lambda (var) + (cond [(opensmtpd-table-configuration-data this-record) + (if (list-of-strings? (opensmtpd-table-configuration-data this-record)) + (quote list-of-strings) + (quote assoc-list))] + [(file-exists? (opensmtpd-table-configuration-data this-record)) + (if (opensmtpd-table-configuration-file-db this-record) + (quote db) + (quote file))] + [else + (display "opensmtpd-table-configuration-type is broke\n") + (throw 'bad! var)]))))) + +(define-record-type* + opensmtpd-ca-configuration make-opensmtpd-ca-configuration + opensmtpd-ca-configuration? + (name opensmtpd-ca-configuration-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-ca-configuration" "name" (list string?))))) + (file opensmtpd-ca-configuration-file + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-ca-configuration" "file" (list file-exists?)))))) + +(define-record-type* + opensmtpd-pki-configuration make-opensmtpd-pki-configuration + opensmtpd-pki-configuration? + (domain opensmtpd-pki-configuration-domain + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-pki-configuration" "domain" (list string?))))) + ;; TODO/FIXME this should probably be a list of files. The opensmtpd documentation says + ;; that you could have a list of files: + ;; + ;; pki pkiname cert certfile + ;; Associate certificate file certfile with host pkiname, and use that file to prove + ;; the identity of the mail server to clients. pkiname is the server's name, de‐ + ;; rived from the default hostname or set using either + ;; /gnu/store/2d13sdz76ldq8zgwv4wif0zx7hkr3mh2-opensmtpd-6.8.0p2/etc/mailname or us‐ + ;; ing the hostname directive. If a fallback certificate or SNI is wanted, the ‘*’ + ;; wildcard may be used as pkiname. + + ;; A certificate chain may be created by appending one or many certificates, includ‐ + ;; ing a Certificate Authority certificate, to certfile. The creation of certifi‐ + ;; cates is documented in starttls(8). + (cert opensmtpd-pki-configuration-cert + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-pki-configuration" "cert" (list file-exists?))))) + (key opensmtpd-pki-configuration-key + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-pki-configuration" "key" (list file-exists?))))) + ; todo sanitize this. valid parameters are "none", "legacy", or "auto". + (dhe opensmtpd-pki-configuration-dhe + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-dhe" "dhe" (list false? string?)))))) + +(define-record-type* + opensmtpd-lmtp-configuration make-opensmtpd-lmtp-configuration + opensmtpd-lmtp-configuration? + (destination opensmtpd-lmtp-configuration-destination + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-lmtp-configuration" "destination" + (list string?))))) + (rcpt-to opensmtpd-lmtp-configuration-rcpt-to + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-lmtp-configuration" "rcpt-to" + (list false? string?)))))) + +(define-record-type* + opensmtpd-mda-configuration make-opensmtpd-mda-configuration + opensmtpd-mda-configuration? + (name opensmtpd-mda-configuration-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-mda-configuration" "name" + (list string?))))) + ;; TODO should I allow this command to be a gexp? + (command opensmtpd-mda-configuration-command + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-mda-configuration" "command" + (list string?)))))) + +(define-record-type* + opensmtpd-maildir-configuration make-opensmtpd-maildir-configuration + opensmtpd-maildir-configuration? + (pathname opensmtpd-maildir-configuration-pathname + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-maildir-configuration" "pathname" + (list false? string?))))) + (junk opensmtpd-maildir-configuration-junk + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-maildir-configuration" "junk" + (list boolean?)))))) + +(define-record-type* + opensmtpd-action-local-delivery-configuration make-opensmtpd-action-local-delivery-configuration + opensmtpd-action-local-delivery-configuration? + (name opensmtpd-action-local-delivery-configuration-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-local-delivery-configuration" "name" + (list string?))))) + (method opensmtpd-action-local-delivery-configuration-method + (default "mbox") + (sanitize (lambda (var) + (cond + [(or (opensmtpd-lmtp-configuration? var) + (opensmtpd-maildir-configuration? var) + (opensmtpd-mda-configuration? var) + (string=? var "mbox") + (string=? var "expand-only") + (string=? var "forward-only")) + var] + [else + (begin + (display (string-append " fieldname 'method' must be of type \n" + "\"mbox\", \"expand-only\", \"forward-only\" \n" + ", , \n" + "or .\n")) + (throw 'bad! var))])))) + (alias opensmtpd-action-local-delivery-configuration-alias + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-local-delivery-configuration" "alias" + (list false? opensmtpd-table-configuration?))))) + (ttl opensmtpd-action-local-delivery-configuration-ttl + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-local-delivery-configuration" "ttl" + (list false? string?))))) + (user opensmtpd-action-local-delivery-configuration-user + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-local-delivery-configuration" "user" + (list false? string?))))) + (userbase opensmtpd-action-local-delivery-configuration-userbase + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-local-delivery-configuration" "userbase" + (list false? opensmtpd-table-configuration?))))) + (virtual opensmtpd-action-local-delivery-configuration-virtual + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-local-delivery-configuration" "virtual" + (list false? opensmtpd-table-configuration?))))) + (wrapper opensmtpd-action-local-delivery-configuration-wrapper + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-local-delivery-configuration" "wrapper" + (list false? string?)))))) + +;; FIXME/TODO this is a valid opensmtpd-relay record +;; (opensmtpd-action-relay-configuration +;; (pki (opensmtpd-pki-configuration +;; (domain "gnucode.me") +;; (cert "opensmtpd.scm") +;; (key "opensmtpd.scm")))) +;; BUT how does it relay the email? What host does it use? +;; I think opensmtpd-relay-configuration needs "method" field. +;; the method field might need to be another record...BUT basically the relay has to have a 'backup', 'backup-mx', +;; or 'domain', or 'host' defined. +(define-record-type* + opensmtpd-action-relay-configuration make-opensmtpd-action-relay-configuration + opensmtpd-action-relay-configuration? + (name opensmtpd-action-relay-configuration-name + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "name" + (list string?)))) + (default #f)) + (backup opensmtpd-action-relay-configuration-backup ;; boolean + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "backup" + (list boolean?))))) + (backup-mx opensmtpd-action-relay-configuration-backup-mx ;; string mx name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "backup-mx" + (list false? string?))))) + (helo opensmtpd-action-relay-configuration-helo + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "helo" + (list false? string? opensmtpd-table-configuration?)))) + (default #f)) + (helo-src opensmtpd-action-relay-configuration-helo-src + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "helo-src" + (list false? string? opensmtpd-table-configuration?)))) + (default #f)) + (domain opensmtpd-action-relay-configuration-domain + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "domain" + (list false? opensmtpd-table-configuration?)))) + (default #f)) + (host opensmtpd-action-relay-configuration-host + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "host" + (list false? string?)))) + (default #f)) + (pki opensmtpd-action-relay-configuration-pki + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "pki" + (list false? opensmtpd-pki-configuration?))))) + (srs opensmtpd-action-relay-configuration-srs + (default #f) + (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "srs" + (list boolean?)))) + (tls opensmtpd-action-relay-configuration-tls + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "tls" + (list false? string?))))) + (auth opensmtpd-action-relay-configuration-auth + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "auth" + (list false? opensmtpd-table-configuration?)))) + (default #f)) + (mail-from opensmtpd-action-relay-configuration-mail-from + (default #f)) + ;; string "127.0.0.1" or "" or "" + ;; TODO should I do some sanitizing to make sure that the string? here is actually an IP address or a valid interface? + (src opensmtpd-action-relay-configuration-src + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "src" + (list false? string? opensmtpd-table-configuration?)))) + (default #f))) + +;; this record is used by & +;; +(define-record-type* + opensmtpd-option-configuration make-opensmtpd-option-configuration + opensmtpd-option-configuration? + (option opensmtpd-option-configuration-option + (default #f) + (sanitize (lambda (var) + (if (and (string? var) + (or (string-in-list? var (list "fcrdns" "rdns" + "src" "helo" + "auth" "mail-from" + "rcpt-to" + "for" + "for any" "for local" + "for domain" "for rcpt-to" + "from any" "from auth" + "from local" "from mail-from" + "from rdns" "from socket" + "from src" "auth" + "helo" "mail-from" + "rcpt-to" "tag" "tls" + )))) + var + (begin + (display (string-append " fieldname: 'option' is of type \n" + "string. The string can be either 'fcrdns', \n" + " 'rdns', 'src', 'helo', 'auth', 'mail-from', or 'rcpt-to', \n" + "'for', 'for any', 'for local', 'for domain', 'for rcpt-to', \n" + "'from any', 'from auth', 'from local', 'from mail-from', 'from rdns', 'from socket', \n" + "'from src', 'auth helo', 'mail-from', 'rcpt-to', 'tag', or 'tls' \n" + )) + (throw 'bad! var)))))) + (not opensmtpd-option-configuration-not + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-option-configuration" "not" + (list boolean?))))) + (regex opensmtpd-option-configuration-regex + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-option-configuration" "regex" + (list boolean?))))) + (data opensmtpd-option-configuration-data + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-option-configuration" "data" + (list false? string? opensmtpd-table-configuration?)))))) + +(define-record-type* + opensmtpd-filter-phase-configuration make-opensmtpd-filter-phase-configuration + opensmtpd-filter-phase-configuration? + (name opensmtpd-filter-phase-configuration-name ;; string chain-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter-phase-configuration" "name" + (list string?))))) + (phase opensmtpd-filter-phase-configuration-phase ;; string + (default #f) + (sanitize (lambda (var) + ;;(my/sanitize var "opensmtpd-filter-phase-configuration" "phase" + ;; (list (sanitize-configuration + ;; (proc (lambda (value) + ;; (and (string? var) + ;; (string-in-list? var (list "connect" + ;; "helo" + ;; "mail-from" + ;; "rcpt-to" + ;; "data" + ;; "commit"))))) + ;; (error-message (list + ;; " fieldname: 'phase' is of type \n" + ;; "string. The string can be either 'connect'," + ;; " 'helo', 'mail-from', 'rcpt-to', 'data', or 'commit.'\n "))))) + (if (and (string? var) + (string-in-list? var (list "connect" + "helo" + "mail-from" + "rcpt-to" + "data" + "commit"))) + var + (begin + (display (string-append " fieldname: 'phase' is of type \n" + "string. The string can be either 'connect'," + " 'helo', 'mail-from', 'rcpt-to', 'data', or 'commit.'\n " + )) + (throw 'bad! var))) + ))) + + (options opensmtpd-filter-phase-configuration-options + (default #f) + (sanitize (lambda (var) + ;; returns #t if list is a unique list of + (define (list-of-opensmtpd-option-configuration? list) + (and (list-of-type? list opensmtpd-option-configuration?) + (not (contains-duplicate? list)))) + + (define (list-has-duplicates-or-non-opensmtpd-option-configuration list) + (not (list-of-opensmtpd-option-configuration? list))) + + ;; input + ;; return #t if fieldname 'option' + ;; that needs a corresponding table has one. Otherwise #f + (define (opensmtpd-option-configuration-has-table? record) + (define decision (opensmtpd-option-configuration-option record)) + (and (string? decision) + ;; if option needs a table, check for a table + (if (string-in-list? decision (list "src" + "helo" + "mail-from" + "rcpt-to")) + (opensmtpd-table-configuration? (opensmtpd-option-configuration-data record)) + #t))) + + (define (list-of-opensmtpd-option-configuration-has-table? list) + (list-of-type? list opensmtpd-option-configuration-has-table?)) + + (define (some-opensmtpd-option-configuration-in-list-lack-table? list) + (not (list-of-opensmtpd-option-configuration-has-table? list))) + + ;;each element in list is of type + (cond [(list-has-duplicates-or-non-opensmtpd-option-configuration var) + (begin + (display (string-append " fieldname: 'options' is a list of unique \n" + " records.\n")) + (throw 'bad! var))] + ;; if fieldname 'option' is of string 'src', 'helo', 'mail-from', 'rcpt-to', then there should be a table + [(some-opensmtpd-option-configuration-in-list-lack-table? var) + (begin + (display (string-append "'s fieldname 'option' values of \n" + "'src', 'helo', 'mail-from', or 'rcpt-to' need a corresponding 'table' \n" + " of type . eg: \n" + "(opensmtpd-option-configuration \n" + " (option \"src\")\n" + " (table (opensmtpd-table-configuration \n" + " (name \"src-table\")\n" + " (data (list \"hello\" \"cat\")))))\n")) + ;; TODO it would be nice if the var this error message throws in the bad + ;; , instead of the list of records. + (throw 'bad! var))] + [else var])))) + (decision opensmtpd-filter-phase-configuration-decision + (default #f) + (sanitize (lambda (var) + (if (and (string? var) + (string-in-list? var (list "bypass" "disconnect" + "reject" "rewrite" "junk"))) + var + (begin + (display (string-append " fieldname: 'decision' is of type \n" + "string. The string can be either 'bypass'," + " 'disconnect', 'reject', 'rewrite', or 'junk'.\n")) + (throw 'bad! var)))))) + (message opensmtpd-filter-phase-configuration-message + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter-phase-configuration" "message" + (list false? string?))))) + (value opensmtpd-filter-phase-configuration-value + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter-phase-configuration" "value" + (list false? number?)))))) + +(define-record-type* + opensmtpd-filter-configuration make-opensmtpd-filter-configuration + opensmtpd-filter-configuration? + (name opensmtpd-filter-configuration-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter" "name" + (list string?))))) + (exec opensmtpd-filter-exec + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter" "exec" + (list boolean?))))) + (proc opensmtpd-filter-configuration-proc ; a string like "rspamd" or the command to start it like "/path/to/rspamd --option=arg --2nd-option=arg2" + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter" "proc" + (list string?)))))) + +;; There is another type of filter that opensmtpd supports, which is a filter chain. +;; A filter chain is a list of and . +;; This lets you apply several filters under one filter name. I could have defined +;; a record type for it, but the record would only have had two fields: name and list-of-filters. +;; Why write that as a record? That's too simple. +;; returns #t if list is a unique list of or +;; returns # otherwise +(define (opensmtpd-filter-chain? %filters) + (and (list-of-unique-filter-or-filter-phase? %filters) + (< 1 (length %filters)))) + +(define-record-type* + opensmtpd-listen-on-configuration make-opensmtpd-listen-on-configuration + opensmtpd-listen-on-configuration? + ;; interface may be an IP address, interface group, or domain name + (interface opensmtpd-listen-on-configuration-interface + (default "lo")) + (family opensmtpd-listen-on-configuration-family + (default #f) + (sanitize (lambda (var) + (cond + [(eq? #f var) ;; var == #f + var] + [(and (string? var) + (string-in-list? var (list "inet4" "inet6"))) + var] + [else + (begin + (display " fieldname 'family' must be string \"inet4\" or \"inet6\".\n") + (throw 'bad! var))])))) + (auth opensmtpd-listen-on-configuration-auth + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-listen-on-configuration" "auth" + (list boolean? table-whose-data-are-assoc-list?))))) + (auth-optional opensmtpd-listen-on-configuration-auth-optional + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-listen-on-configuration" "auth-optional" + (list boolean? + table-whose-data-are-assoc-list?))))) + ;; TODO add a ca entry? + ;; string FIXME/TODO sanitize this to support a gexp. That way way the + ;; includes directive can include my hacky scheme code that I use for opensmtpd-dkimsign. + (filters opensmtpd-listen-on-configuration-filters + (default #f) + (sanitize (lambda (var) + (sanitize-filters var)))) + (hostname opensmtpd-listen-on-configuration-hostname + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-listen-on-configuration" "hostname" + (list false? string?))))) + (hostnames opensmtpd-listen-on-configuration-hostnames + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-listen-on-configuration" "hostnames" + (list false? table-whose-data-are-assoc-list?))))) + (mask-src opensmtpd-listen-on-configuration-mask-src + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-listen-on-configuration" "mask-src" + (list boolean?))))) + (disable-dsn opensmtpd-listen-on-configuration-disable-dsn + (default #f)) + (pki opensmtpd-listen-on-configuration-pki + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-listen-on-configuration" "pki" + (list false? opensmtpd-pki-configuration?))))) + (port opensmtpd-listen-on-configuration-port + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-listen-on-configuration" "port" + (list false? integer?))))) + (proxy-v2 opensmtpd-listen-on-configuration-proxy-k2 + (default #f)) + (received-auth opensmtpd-listen-on-configuration-received-auth + (default #f)) + ;; TODO add in a senders option! + ;; string or record + ;; (senders opensmtpd-listen-on-configuration-senders + ;; (sanitize (lambda (var) + ;; (my/sanitize var "opensmtpd-listen-on-configuration" "port" (list false? integer?)))) + ;; (default #f)) + (secure-connection opensmtpd-listen-on-configuration-secure-connection + (default #f) + (sanitize (lambda (var) + (cond [(boolean? var) + var] + [(and (string? var) + (string-in-list? var + (list "smtps" "tls" + "tls-require" + "tls-require-verify"))) + var] + [else + (begin + (display (string-append " fieldname 'secure-connection' can be \n" + "one of the following strings: \n'smtps', 'tls', 'tls-require', \n" + "or 'tls-require-verify'.\n")) + (throw 'bad! var))])))) + (tag opensmtpd-listen-on-configuration-tag + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-listen-on-configuration" "tag" + (list false? string?)))) + (default #f))) + +(define-record-type* + opensmtpd-listen-on-socket-configuration-configuration make-opensmtpd-listen-on-socket-configuration-configuration + opensmtpd-listen-on-socket-configuration-configuration? + ;; false or or list of + (filters opensmtpd-listen-on-socket-configuration-configuration-filters + (sanitize (lambda (var) + (sanitize-filters var))) + (default #f)) + (mask-src opensmtpd-listen-on-socket-configuration-configuration-mask-src + (default #f)) + (tag opensmtpd-listen-on-socket-configuration-configuration-tag + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-listen-on-configuration" "tag" + (list false? string?)))) + (default #f))) + + +(define-record-type* + opensmtpd-match-configuration make-opensmtpd-match-configuration + opensmtpd-match-configuration? + ;;TODO? Perhaps I should add in a reject fieldname. If reject + ;;is #t, then the match record will be a reject match record. + ;; (opensmtpd-match (reject #t)) vs. (opensmtpd-match (action 'reject)) + ;; To do this, I will also have to 'reject' mutually exclusive. AND an match with 'reject' can have no action defined. + (action opensmtpd-match-configuration-action + (default #f) + (sanitize (lambda (var) + (if (or (opensmtpd-action-relay-configuration? var) + (opensmtpd-action-local-delivery-configuration? var) + (eq? (quote reject) var)) + var + (begin + (display + (string-append " fieldname 'action' is of type , \n" + ", or (quote reject).\n" + "If its var is (quote reject), then the match rejects the incoming message\n" + "during the SMTP dialogue.\n")) + (throw 'bad! var)))))) + (options opensmtpd-match-configuration-options + (default #f) + (sanitize (lambda (var) + (cond ((not var) + #f) + ((not (list-of-unique-opensmtpd-option-configuration? var)) + (throw-error var '(" fieldname 'options' is a list of unique \n" + " records. \n"))) + (else (sanitize-list-of-options-for-match-configuration var))))))) + +(define-record-type* + opensmtpd-smtp-configuration make-opensmtpd-smtp-configuration + opensmtpd-smtp-configuration? + (ciphers opensmtpd-smtp-configuration-ciphers + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp-configuration" "ciphers" + (list false? string?))))) + (limit-max-mails opensmtpd-smtp-configuration-limit-max-mails + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp-configuration" "limit-max-mails" + (list false? integer?))))) + (limit-max-rcpt opensmtpd-smtp-configuration-limit-max-rcpt + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp-configuration" "limit-max-rcpt" + (list false? integer?))))) + (max-message-size opensmtpd-smtp-configuration-max-message-size + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp-configuration" "max-message-size" + (list false? integer? string?))))) + ;; FIXME/TODO the sanitize function of sub-addr-delim should accept a string of length one not string? + (sub-addr-delim opensmtpd-smtp-configuration-sub-addr-delim + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp-configuration" "sub-addr-delim" + (list false? integer? string?)))))) + +(define-record-type* + opensmtpd-srs-configuration make-opensmtpd-srs-configuration + opensmtpd-srs-configuration? + ;; TODO should this be a file? + (key opensmtpd-srs-configuration-key + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-srs-configuration" "key" + (list false? boolean? string?))))) + ;; TODO should this also be a file? + (backup-key opensmtpd-srs-configuration-backup-key + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-srs-configuration" "backup-key" + (list false? integer?))))) + (ttl-delay opensmtpd-srs-configuration-ttl-delay + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-srs-configuration" "ttl-delay" + (list false? string?)))))) + +(define-record-type* + opensmtpd-queue-configuration make-opensmtpd-queue-configuration + opensmtpd-queue-configuration? + (compression opensmtpd-queue-configuration-compression + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-queue-configuration" "compression" + (list boolean?))))) + (encryption opensmtpd-queue-configuration-encryption + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-queue-configuration" "encryption" + (list boolean? file-exists? string?))))) + (ttl-delay opensmtpd-queue-configuration-ttl-delay + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-queue-configuration" "ttl-delay" + (list false? string?)))))) + (define-record-type* opensmtpd-configuration make-opensmtpd-configuration opensmtpd-configuration? - (package opensmtpd-configuration-package - (default opensmtpd)) + (package opensmtpd-configuration-package + (default opensmtpd)) (config-file opensmtpd-configuration-config-file - (default %default-opensmtpd-config-file))) + (default #f)) + ;; FIXME/TODO should I include a admd authservid entry? + + ;; TODO sanitize this properly with perhaps a . + (bounce opensmtpd-configuration-bounce + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "bounce" + (list false? list?))))) + (cas opensmtpd-configuration-cas + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "cas" + (list false? list-of-opensmtpd-ca-configuration?))))) + ;; list of many records of type opensmtpd-listen-on-configuration + (listen-ons opensmtpd-configuration-listen-ons + (default (list (opensmtpd-listen-on-configuration))) + (sanitize (lambda (var) + (if (list-of-opensmtpd-listen-on-configuration? var) + var + (begin + (display " fieldname 'listen-ons' expects a list of records ") + (display "of one or more unique records.\n") + (throw 'bad! var)))))) + ;; accepts type + (listen-on-socket opensmtpd-configuration-listen-on-socket + (default (opensmtpd-listen-on-socket-configuration-configuration))) + (includes opensmtpd-configuration-includes ;; list of strings of absolute path names + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "includes" + (list false? list-of-strings?))))) + (matches opensmtpd-configuration-matches + (default (list (opensmtpd-match-configuration + (action (opensmtpd-action-local-delivery-configuration + (name "local") + (method "mbox"))) + (options (list + (opensmtpd-option-configuration + (option "for local"))))) + (opensmtpd-match-configuration + (action (opensmtpd-action-relay-configuration + (name "outbound"))) + (options (list + (opensmtpd-option-configuration + (option "from local")) + (opensmtpd-option-configuration + (option "for any"))))))) + ;; TODO perhaps I should sanitize this function like I sanitized the 'filters'. + ;; I definitely should sanitize this function a bit more. For example, you could have two different + ;; actions, one for local delivery and one for remote, with the same name. I should make sure that + ;; I have no two different actions with the same name. + (sanitize (lambda (var) + ;; Should we do more sanitizing here? eg: "from socket" should NOT have a table or value + var + (my/sanitize var "opensmtpd-configuration" "matches" + (list list-of-unique-opensmtpd-match-configuration?))))) + ;; list of many records of type mda-wrapper + ;; TODO/FIXME support using gexps here + ;; eg (list "name" gexp) + (mda-wrappers opensmtpd-configuration-mda-wrappers + (default #f) + (sanitize (lambda (var) + (my/sanitize var + "opensmtpd-configuration" + "mda-wrappers" + (list false? string?))))) + (mta-max-deferred opensmtpd-configuration-mta-max-deferred + (default 100) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "mta-max-deferred" + (list number?))))) + + ;; TODO should I add a fieldname proc _proc-name_ _command_ as found in the man 5 smtpd.conf ? + + (queue opensmtpd-configuration-queue + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "queue" + (list false? opensmtpd-queue-configuration?))))) + (smtp opensmtpd-configuration-smtp + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "smtp" + (list false? opensmtpd-smtp-configuration?))))) + (srs opensmtpd-configuration-srs + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "srs" + (list false? opensmtpd-srs-configuration?)))))) + +;; This is a non-exported record for passing around sanitize procedures. +;; As of 5/2/2022 I am not using it. I should probably just delete it. +(define-record-type* + sanitize-configuration make-sanitize-configuration + sanitize-configuration? + (proc sanitize-configuration-proc + (default #f) + ;;(sanitize (lambda (var) (procedure? var))) + ) + (args sanitize-configuration-args + (default #f) + ;;(sanitize (lambda (var) (lambda (var) (list? var)))) + ) + (error-message sanitize-configuration-error-message + (default #f) + ;;(sanitize (lambda (var) (list? var))) + ) + (error-if-proc-fails sanitize-configuration-error-if-proc-fails + (default #f))) + +;; this help procedure is used 3 or 4 times by sanitize-list-of-options-for-match-configuration +(define (throw-error-duplicate-option option error-arg) + (throw-error error-arg + (list "'s fieldname 'options' has two\n" + (string-append " records with fieldname 'option' with value '" option "'. \n") + (string-append "You can only have one option with value '" option "' in the options list.\n")))) + +;; this procedure sanitizes the fieldname opensmtpd-match-configuration-options +(define* (sanitize-list-of-options-for-match-configuration %options) + (let loop ([%traversing-options %options] + [%sanitized-options '()]) + (if (null? %traversing-options) + (remove false? + (list + (assoc-ref %sanitized-options "for") + (assoc-ref %sanitized-options "from") + (assoc-ref %sanitized-options "auth") + (assoc-ref %sanitized-options "helo") + (assoc-ref %sanitized-options "mail-from") + (assoc-ref %sanitized-options "rcpt-to") + (assoc-ref %sanitized-options "tag") + (assoc-ref %sanitized-options "tls"))) + (let* ((option-record (car %traversing-options)) + (option-string (opensmtpd-option-configuration-option option-record))) + (cond [(string=? "auth" option-string) + (if (assoc-ref %sanitized-options "auth") + (throw-error-duplicate-option "auth" %traversing-options) + (loop (cdr %traversing-options) (alist-cons "auth" option-record %sanitized-options)))] + [(string=? "helo" option-string) + (cond [(assoc-ref %sanitized-options "helo") + (throw-error-duplicate-option "helo" %traversing-options)] + [(not (opensmtpd-option-configuration-data option-record)) + (throw-error option-record + (list " with fieldname 'option' with value 'helo' \n" + "must have a 'data' of type string or .\n"))] + [else (loop (cdr %traversing-options) (alist-cons "helo" option-record %sanitized-options))])] + [(string=? "mail-from" option-string) + (cond ((assoc-ref %sanitized-options "mail-from") + (throw-error-duplicate-option "mail-from" %traversing-options)) + ((not (opensmtpd-option-configuration-data option-record)) + (throw-error option-record + (list " with fieldname 'option' with value 'mail-from' \n" + "must have a 'data' of type string or .\n"))) + (else (loop (cdr %traversing-options) (alist-cons "mail-from" option-record %sanitized-options))))] + [(string=? "rcpt-to" option-string) + (cond [(assoc-ref %sanitized-options "rcpt-to") + (throw-error-duplicate-option "rcpt-to" %traversing-options)] + [(not (opensmtpd-option-configuration-data option-record)) + (throw-error option-record + (list " with fieldname 'option' with value 'rcpt-to' \n" + "must have a 'data' of type string or .\n"))] + [else (loop (cdr %traversing-options) (alist-cons "rcpt-to" option-record %sanitized-options))])] + [(string=? "tag" option-string) + (cond ((assoc-ref %sanitized-options "tag") + (throw-error-duplicate-option "tag" %traversing-options)) + ((not (string? (opensmtpd-option-configuration-data option-record))) + (throw-error option-record + (list " with fieldname 'option' with value 'tag' \n" + "must have a 'data' of type string.\n"))) + (else (loop (cdr %traversing-options) (alist-cons "tag" option-record %sanitized-options))))] + [(string=? "tls" option-string) + (cond [(assoc-ref %sanitized-options "tls") + (throw-error-duplicate-option "tls" %traversing-options)] + [(or (opensmtpd-option-configuration-data option-record) + (opensmtpd-option-configuration-regex option-record)) + (throw-error option-record + (list " with fieldname 'option' with value 'tls' \n" + "cannot have a string or table 'data'.\n"))] + [else (loop (cdr %traversing-options) (alist-cons "tls" option-record %sanitized-options))])] + [(string=? "for" (substring option-string 0 3)) + (cond ((assoc-ref %sanitized-options "for") + (throw-error %options + `("'s fieldname 'options' can only have one 'for' option. \n" + "But '" ,option-string "' and '" + ,(opensmtpd-option-configuration-option (assoc-ref %sanitized-options "for")) "' are present.\n"))) + ((and (string-in-list? option-string (list "for any" "for local")) ; for any cannot have a data field. + (or (opensmtpd-option-configuration-data option-record) + (opensmtpd-option-configuration-regex option-record))) + (throw-error option-record + (list "When 's fieldname 'options' value is 'for any' \n" + "or 'for local', then its 'data' and 'regex' field must be #f. \n"))) + ((and (string-in-list? option-string (list "for domain" "for rcpt-to")) ; for domain must have a data field. + (not (opensmtpd-option-configuration-data option-record))) + (throw-error option-record + (list "When 's fieldname 'options' value is 'for domain' \n" + "or 'for rcpt-to', then its 'data' field must be a string or an \n" + " record.\n"))) + (else (loop (cdr %traversing-options) (alist-cons "for" option-record %sanitized-options))))] + [(string=? "from" (substring option-string 0 4)) + (cond ((assoc-ref %sanitized-options "from") + (throw-error %options + `("'s fieldname 'options' can only have one 'from' option. \n" + "But '" ,option-string "' and '" + ,(opensmtpd-option-configuration-option (assoc-ref %sanitized-options "from")) "' are present.\n"))) + ((and (string-in-list? option-string (list "from any" "from local" "from socket")) ; for any cannot have a data field. + (or (opensmtpd-option-configuration-data option-record) + (opensmtpd-option-configuration-regex option-record))) + (throw-error option-record + (list "When 's fieldname 'options' value is 'from any', \n" + " 'from local', or 'from socket', then its 'data' and 'regex' field must be #f. \n"))) + ((and (string-in-list? option-string (list "from mail-from" "from src")) ; for domain must have a data field. + (not (opensmtpd-option-configuration-data option-record))) + (throw-error option-record + (list "When 's fieldname 'options' value is 'from mail-from' \n" + "or 'from src', then its 'data' field must be a string or an \n" + " record.\n"))) + (else (loop (cdr %traversing-options) (alist-cons "from" option-record %sanitized-options))))]))))) + +;; some procedures for and +;; . +(define (sanitize-filters %list) + ;; the order of the first two tests in this cond is important. + ;; (false?) has to be 1st and (list-has-duplicates-or-non-filters?) has to be second. + ;; You may optionally re-order the other alternates in the cond. + (cond [(false? %list) + #f] + [(list-has-duplicates-or-non-filters? %list) + (begin + (display (string-append " fieldname: 'filters' is a list, in which each unique element \n" + "is of type or .\n")) + (throw 'bad! %list))] + [else + (let loop ([%traversing-list %list] + [%original-list %list]) + (if (null? %traversing-list) + %original-list + (cond + [(opensmtpd-filter-configuration? (car %traversing-list)) + (loop (cdr %traversing-list) %original-list)] + [(filter-phase-has-message-and-value? (car %traversing-list)) + (begin + (display (string-append " cannot have defined fieldnames 'value' \n" + "and 'message'.\n")) + (throw 'bad! (car %traversing-list)))] + [(filter-phase-decision-lacks-proper-message? (car %traversing-list)) + (begin + (display (string-append " fieldname: 'decision' options \n" + "\"disconnect\" and \"reject\" require fieldname 'message' to have a string.\n" + "The 'message' string must be RFC commpliant, which means that the string \n" + "must begin with a 4xx or 5xx status code.\n")) + (throw 'bad! (car %traversing-list)))] + [(filter-phase-lacks-proper-value? (car %traversing-list)) + (begin + (display (string-append " fieldname: 'decision' option \n" + "\"rewrite\" requires fieldname 'value' to have a number.\n")) + (throw 'bad! (car %traversing-list)))] + [(filter-phase-has-incorrect-junk-or-bypass? (car %traversing-list)) + (begin + (display (string-append " fieldname 'decision' option \n" + "\"junk\" or 'bypass' cannot have a defined fieldnames 'message' or 'value'.\n")) + (throw 'bad! (car %traversing-list)))] + [(filter-phase-junks-after-commit? (car %traversing-list)) + (begin + (display (string-append " fieldname 'decision' option \n" + "\"junk\" cannot junk an email during 'phase' \"commit\".\n")) + (throw 'bad! (car %traversing-list)))] + [else (loop (cdr %traversing-list) %original-list)])))])) + +(define (list-has-duplicates-or-non-filters? list) + (not (list-of-unique-filter-or-filter-phase? list))) + +(define (filter-phase-has-message-and-value? record) + (and (opensmtpd-filter-phase-configuration-message record) + (opensmtpd-filter-phase-configuration-value record))) + +;; return #t if phase needs a message. Or if the message did not start with a 4xx or 5xx status code. +;; otherwise #f +(define (filter-phase-decision-lacks-proper-message? record) + (define decision (opensmtpd-filter-phase-configuration-decision record)) + (if (string-in-list? decision (list "disconnect" "reject")) + ;; this message needs to be RFC compliant, meaning + ;; that it need to start with 4xx or 5xx status code + (cond [(eq? #f (opensmtpd-filter-phase-configuration-message record)) + #t] + [(string? (opensmtpd-filter-phase-configuration-message record)) + (let ((number (string->number + (substring + (opensmtpd-filter-phase-configuration-message record) 0 3)))) + (if (and (number? number) + (and (< number 600) (> number 399))) + #f + #t))]) + #f)) + +;; 'decision' "rewrite" requires 'value' to be a number. +(define (filter-phase-lacks-proper-value? record) + (define decision (opensmtpd-filter-phase-configuration-decision record)) + (if (string=? "rewrite" decision) + (if (and (number? (opensmtpd-filter-phase-configuration-value record)) + (eq? #f (opensmtpd-filter-phase-configuration-message record))) + #f + #t) + #f)) + +;; 'decision' "junk" or "bypass" cannot have a message or a value. +(define (filter-phase-has-incorrect-junk-or-bypass? record) + (and + (string-in-list? + (opensmtpd-filter-phase-configuration-decision record) + (list "junk" "bypass")) + (or + (opensmtpd-filter-phase-configuration-value record) + (opensmtpd-filter-phase-configuration-message record)))) + +(define (filter-phase-junks-after-commit? record) + (and (string=? (opensmtpd-filter-phase-configuration-decision record) "junk") + (string=? (opensmtpd-filter-phase-configuration-phase record) "commit"))) + +;; returns #t if list is a unique list of or +;; returns # otherwise +(define (list-of-unique-filter-or-filter-phase? %filters) + (and (list? %filters) + (not (null? %filters)) + ;; this list is made up of only or + (primitive-eval + (cons 'and (map (lambda (filter) + (or (opensmtpd-filter-configuration? filter) + (opensmtpd-filter-phase-configuration? filter))) + %filters))) + (not (contains-duplicate? %filters)))) + +(define (throw-error var %strings) + (display (apply string-append %strings)) + (throw 'bad! var)) + +;; this is used for sanitizing fieldname 'options' +(define (contains-duplicate? list) + (if (null? list) + #f + (or + ;; check if (car list) is in (cdr list) + (primitive-eval (cons 'or + (map (lambda (var) (equal? var (car list))) + (cdr list)))) + ;; check if (cdr list) contains duplicate + (contains-duplicate? (cdr list))))) + +;; given a list and procedure, this tests that each element of list is of type +;; ie: (list-of-type? list string?) tests each list is of type string. +(define (list-of-type? list proc?) + (if (and (list? list) + (not (null? list))) + (let loop ([list list]) + (if (null? list) + #t + (if (proc? (car list)) + (loop (cdr list)) + #f))) + #f)) + +(define (list-of-strings? list) + (list-of-type? list string?)) + +(define (list-of-unique-opensmtpd-option-configuration? list) + (and (list-of-type? + list opensmtpd-option-configuration?) + (not (contains-duplicate? list)))) + +(define (list-of-opensmtpd-ca-configuration? list) + (list-of-type? list opensmtpd-ca-configuration?)) + +(define (list-of-opensmtpd-pki-configuration? list) + (list-of-type? list opensmtpd-pki-configuration?)) + +(define (list-of-opensmtpd-listen-on-configuration? list) + (and (list-of-type? list opensmtpd-listen-on-configuration?) + (not (contains-duplicate? list)))) + +(define (list-of-unique-opensmtpd-match-configuration? list) + (and (list-of-type? list opensmtpd-match-configuration?) + (not (contains-duplicate? list)))) + +(define* (list-of-strings->string list + #:key + (string-delimiter ", ") + (postpend "") + (append "") + (drop-right-number 2)) + (string-drop-right + (string-append (let loop ([list list]) + (if (null? list) + "" + (string-append append (car list) postpend + string-delimiter + (loop (cdr list))))) + append) + drop-right-number)) + +;; at the moment I cannot define this by using list-of-type? +;; the first (not (null? assoc-list)) prevents that. +(define (assoc-list? assoc-list) + (list-of-type? assoc-list (lambda (pair) + (if (and (pair? pair) + (string? (car pair)) + (string? (cdr pair))) + #t + #f)))) + +(define* (variable->string var #:key (append "") (postpend " ")) + (let ([var (if (number? var) + (number->string var) + var)]) + (if var + (string-append append var postpend) + ""))) + +;; this procedure takes in one argument. +;; if that argument is an whose fieldname 'values' is an assoc-list, then it returns +;; #t, #f if otherwise. +;; TODO should I remove these two functions? And instead use the (opensmtpd-table-configuration-type) procedure? +(define (table-whose-data-are-assoc-list? table) + (if (not (opensmtpd-table-configuration? table)) + #f + (assoc-list? (opensmtpd-table-configuration-data table)))) + +;; this procedure takes in one argument +;; if that argument is an whose fieldname 'values' is a list of strings, then it returns +;; #t, #f if otherwise. +(define (table-whose-data-are-a-list-of-strings? table) + (if (not (opensmtpd-table-configuration? table)) + #f + (list-of-strings? (opensmtpd-table-configuration-data table)))) + +;; these next few functions help me to turn
s +;; into strings suitable to fit into "opensmtpd.conf". +(define (assoc-list->string assoc-list) + (string-drop-right + (let loop ([assoc-list assoc-list]) + (if (null? assoc-list) + "" + ;; pair is (cons "hello" "world") -> ("hello" . "world") + (let ([pair (car assoc-list)]) + (string-append + "\"" (car pair) "\"" + " = " + "\"" (cdr pair) "\"" + ", " + (loop (cdr assoc-list)))))) + 2)) + +;; can be of type: (quote list-of-strings) or (quote assoc-list) +(define (opensmtpd-table-configuration->string table) + (string-append "table " (opensmtpd-table-configuration-name table) " " + (let ([type (opensmtpd-table-configuration-type table)]) + (cond [(eq? type (quote list-of-strings)) + (string-append "{ " (list-of-strings->string (opensmtpd-table-configuration-data table) + #:append "\"" + #:drop-right-number 3 + #:postpend "\"") " }")] + [(eq? type (quote assoc-list)) + (string-append "{ " (assoc-list->string (opensmtpd-table-configuration-data table)) " }")] + [(eq? type (quote db)) + (string-append "db:" (opensmtpd-table-configuration-data table))] + [(eq? type (quote file)) + (string-append "file:" (opensmtpd-table-configuration-data table))] + [else (throw 'youMessedUp table)])) + " \n")) + +;; The following functions convert various records into strings. + +(define (opensmtpd-listen-on-configuration->string record) + (string-append "listen on " + (opensmtpd-listen-on-configuration-interface record) " " + (let* ([hostname (opensmtpd-listen-on-configuration-hostname record)] + [hostnames (if (opensmtpd-listen-on-configuration-hostnames record) + (opensmtpd-table-configuration-name (opensmtpd-listen-on-configuration-hostnames record)) + #f)] + [filters (opensmtpd-listen-on-configuration-filters record)] + [filter-name (if filters + (if (< 1 (length filters)) + (generate-filter-chain-name filters) + (if (opensmtpd-filter-configuration? (car filters)) + (opensmtpd-filter-configuration-name (car filters)) + (opensmtpd-filter-phase-configuration-name (car filters)))) + #f)] + [mask-src (opensmtpd-listen-on-configuration-mask-src record)] + [tag (opensmtpd-listen-on-configuration-tag record)] + [secure-connection (opensmtpd-listen-on-configuration-secure-connection record)] + [port (opensmtpd-listen-on-configuration-port record)] + [pki (opensmtpd-listen-on-configuration-pki record)] + [auth (opensmtpd-listen-on-configuration-auth record)] + [auth-optional (opensmtpd-listen-on-configuration-auth-optional record)]) + (string-append + (if mask-src + (string-append "mask-src ") + "") + (variable->string hostname #:append "hostname ") + (variable->string hostnames #:append "hostnames <" #:postpend "> ") + (variable->string filter-name #:append "filter \"" #:postpend "\" ") + (variable->string tag #:append "tag \"" #:postpend "\" ") + (if secure-connection + (cond [(string=? "smtps" secure-connection) + "smtps "] + [(string=? "tls" secure-connection) + "tls "] + [(string=? "tls-require" secure-connection) + "tls-require "] + [(string=? "tls-require-verify" secure-connection) + "tls-require verify "]) + "") + (variable->string port #:append "port " #:postpend " ") + (if pki + (variable->string (opensmtpd-pki-configuration-domain pki) #:append "pki ") + "") + (if auth + (string-append "auth " + (if (opensmtpd-table-configuration? auth) + (string-append "<" (opensmtpd-table-configuration-name auth) "> ") + "")) + "") + (if auth-optional + (string-append "auth-optional " + (if (opensmtpd-table-configuration? auth-optional) + (string-append "<" (opensmtpd-table-configuration-name auth-optional) "> ") + "")) + "") + "\n")))) + +(define (opensmtpd-listen-on-socket-configuration->string record) + (string-append "listen on socket " + (let* ([filters (opensmtpd-listen-on-socket-configuration-configuration-filters record)] + [filter-name (if filters + (if (< 1 (length filters)) + (generate-filter-chain-name filters) + (if (opensmtpd-filter-configuration? (car filters)) + (opensmtpd-filter-configuration-name (car filters)) + (opensmtpd-filter-phase-configuration-name (car filters)))) + #f)] + [mask-src (opensmtpd-listen-on-socket-configuration-configuration-mask-src record)] + [tag (opensmtpd-listen-on-socket-configuration-configuration-tag record)]) + (string-append + (if mask-src + (string-append "mask-src ") + "") + (variable->string filter-name #:append "filter \"" #:postpend "\" ") + (variable->string tag #:append "tag \"" #:postpend "\" ") + "\n")))) + +(define (opensmtpd-action-relay-configuration->string record) + (let ([backup (opensmtpd-action-relay-configuration-backup record)] + [backup-mx (opensmtpd-action-relay-configuration-backup-mx record)] + [helo (opensmtpd-action-relay-configuration-helo record)] + ;; helo-src can either be a string IP address or an + [helo-src (if (opensmtpd-action-relay-configuration-helo-src record) + (if (string? (opensmtpd-action-relay-configuration-helo-src record)) + (opensmtpd-action-relay-configuration-helo-src record) + (string-append "<\"" + (opensmtpd-table-configuration-name + (opensmtpd-action-relay-configuration-src record)) + "\">")) + #f)] + [domain (if (opensmtpd-action-relay-configuration-domain record) + (opensmtpd-table-configuration-name + (opensmtpd-action-relay-configuration-domain record)) + #f)] + [host (opensmtpd-action-relay-configuration-host record)] + [name (opensmtpd-action-relay-configuration-name record)] + [pki (if (opensmtpd-action-relay-configuration-pki record) + (opensmtpd-pki-configuration-domain (opensmtpd-action-relay-configuration-pki record)) + #f)] + [srs (opensmtpd-action-relay-configuration-srs record)] + [tls (opensmtpd-action-relay-configuration-tls record)] + [auth (if (opensmtpd-action-relay-configuration-auth record) + (opensmtpd-table-configuration-name + (opensmtpd-action-relay-configuration-auth record)) + #f)] + [mail-from (opensmtpd-action-relay-configuration-mail-from record)] + ;; src can either be a string IP address or an + [src (if (opensmtpd-action-relay-configuration-src record) + (if (string? (opensmtpd-action-relay-configuration-src record)) + (opensmtpd-action-relay-configuration-src record) + (string-append "<\"" + (opensmtpd-table-configuration-name + (opensmtpd-action-relay-configuration-src record)) + "\">")) + #f)] + ) + (string-append + "\"" + name + "\" " "relay " + ;;FIXME should I always quote the host fieldname? do I need to quote localhost via "localhost" ? + (variable->string host #:append "host \"" #:postpend "\" ") + (variable->string backup) + (variable->string backup-mx #:append "backup mx ") + (variable->string helo #:append "helo ") + (variable->string helo-src #:append "helo-src ") + (variable->string domain #:append "domain <\"" #:postpend "\"> ") + (variable->string host #:append "host ") + (variable->string pki #:append "pki ") + (variable->string srs) + (variable->string tls #:append "tls ") + (variable->string auth #:append "auth <" #:postpend "> ") + (variable->string mail-from #:append "mail-from ") + (variable->string src #:append "src ") + "\n"))) + +(define (opensmtpd-lmtp-configuration->string record) + (string-append "lmtp " + (opensmtpd-lmtp-configuration-destination record) + (if (opensmtpd-lmtp-configuration-rcpt-to record) + (begin + " " (opensmtpd-lmtp-configuration-rcpt-to record)) + ""))) + +(define (opensmtpd-mda-configuration->string record) + (string-append "mda " + (opensmtpd-mda-configuration-command record) " ")) + +(define (opensmtpd-maildir-configuration->string record) + (string-append "maildir " + "\"" + (if (opensmtpd-maildir-configuration-pathname record) + (opensmtpd-maildir-configuration-pathname record) + "~/Maildir") + "\"" + (if (opensmtpd-maildir-configuration-junk record) + " junk " + " "))) + +(define (opensmtpd-action-local-delivery-configuration->string record) + (let ([name (opensmtpd-action-local-delivery-configuration-name record)] + [method (opensmtpd-action-local-delivery-configuration-method record)] + [alias (if (opensmtpd-action-local-delivery-configuration-alias record) + (opensmtpd-table-configuration-name + (opensmtpd-action-local-delivery-configuration-alias record)) + #f)] + [ttl (opensmtpd-action-local-delivery-configuration-ttl record)] + [user (opensmtpd-action-local-delivery-configuration-user record)] + [userbase (if (opensmtpd-action-local-delivery-configuration-userbase record) + (opensmtpd-table-configuration-name + (opensmtpd-action-local-delivery-configuration-userbase record)) + #f)] + [virtual (if (opensmtpd-action-local-delivery-configuration-virtual record) + (opensmtpd-table-configuration-name + (opensmtpd-action-local-delivery-configuration-virtual record)) + #f)] + [wrapper (opensmtpd-action-local-delivery-configuration-wrapper record)]) + (string-append + "\"" name "\" " + (cond [(string? method) + (string-append method " ")] + [(opensmtpd-mda-configuration? method) + (opensmtpd-mda-configuration->string method)] + [(opensmtpd-lmtp-configuration? method) + (opensmtpd-lmtp-configuration->string method)] + [(opensmtpd-maildir-configuration? method) + (opensmtpd-maildir-configuration->string method)]) + ;; FIXME/TODO support specifying alias file:/path/to/alias-file ? + ;; I do not think that is something that I can do... + (variable->string alias #:append "alias <\"" #:postpend "\"> ") + (variable->string ttl #:append "ttl ") + (variable->string user #:append "user ") + (variable->string userbase #:append "userbase <\"" #:postpend "\"> ") + (variable->string virtual #:append "virtual <" #:postpend "> ") + (variable->string wrapper #:append "wrapper ")))) + +;; this function turns both opensmtpd-action-local-delivery-configuration and +;; opensmtpd-action-relay-configuration into strings. +(define (opensmtpd-action->string record) + (string-append "action " + (cond [(opensmtpd-action-local-delivery-configuration? record) + (opensmtpd-action-local-delivery-configuration->string record)] + [(opensmtpd-action-relay-configuration? record) + (opensmtpd-action-relay-configuration->string record)]) + " \n")) + +;; this turns option records found in into strings. +(define* (opensmtpd-option-configuration->string record + #:key + (space-after-! #f)) + (let ([not (opensmtpd-option-configuration-not record)] + [option (opensmtpd-option-configuration-option record)] + [regex (opensmtpd-option-configuration-regex record)] + [data (opensmtpd-option-configuration-data record)]) + (string-append + (if not + (if space-after-! + "! " + "!") + "") + option " " + (if regex + "regex " + "") + (if data + (if (opensmtpd-table-configuration? data) + (string-append "<" (opensmtpd-table-configuration-name data) "> ") + (string-append data " ")) + "")))) + +(define (opensmtpd-match-configuration->string record) + (string-append "match " + (let* ([action (opensmtpd-match-configuration-action record)] + [name (cond [(opensmtpd-action-relay-configuration? action) + (opensmtpd-action-relay-configuration-name action)] + [(opensmtpd-action-local-delivery-configuration? action) + (opensmtpd-action-local-delivery-configuration-name action)] + [else 'reject])] + [options (opensmtpd-match-configuration-options record)]) + (string-append + (if options + (apply string-append + (map opensmtpd-option-configuration->string options)) + "") + (if (string? name) + (string-append "action " "\"" name "\" ") + "reject ") + "\n")))) + +(define (opensmtpd-ca-configuration->string record) + (string-append "ca " (opensmtpd-ca-configuration-name record) " " + "cert \"" (opensmtpd-ca-configuration-file record) "\"\n")) + +(define (opensmtpd-pki-configuration->string record) + (let ([domain (opensmtpd-pki-configuration-domain record)] + [cert (opensmtpd-pki-configuration-cert record)] + [key (opensmtpd-pki-configuration-key record)] + [dhe (opensmtpd-pki-configuration-dhe record)]) + (string-append "pki " domain " " "cert \"" cert "\" \n" + "pki " domain " " "key \"" key "\" \n" + (if dhe + (string-append + "pki " domain " " "dhe " dhe "\n") + "")))) + +(define (generate-filter-chain-name list-of-filters) + (string-drop-right (apply string-append + (flatten + (map (lambda (filter) + (list + (if (opensmtpd-filter-configuration? filter) + (opensmtpd-filter-configuration-name filter) + (opensmtpd-filter-phase-configuration-name filter)) + "-")) + list-of-filters))) + 1)) + +;; this procedure takes in a list of and , +;; returns a string of the form: +;; filter "uniquelyGeneratedName" chain chain { "filter-name", "filter-name2" [, ...]} +(define (opensmtpd-filter-chain->string list-of-filters) + (string-append "filter \"" + (generate-filter-chain-name list-of-filters) + "\" " + "chain {" + (string-drop-right + (apply string-append + (flatten + (map (lambda (filter) + (list + "\"" + (if (opensmtpd-filter-configuration? filter) + (opensmtpd-filter-configuration-name filter) + (opensmtpd-filter-phase-configuration-name filter)) + "\", ")) + list-of-filters)) + ) 2) + "}\n")) + +(define (opensmtpd-filter-phase-configuration->string record) + (let ([name (opensmtpd-filter-phase-configuration-name record)] + [phase (opensmtpd-filter-phase-configuration-phase record)] + [decision (opensmtpd-filter-phase-configuration-decision record)] + [options (opensmtpd-filter-phase-configuration-options record)] + [message (opensmtpd-filter-phase-configuration-message record)] + [value (opensmtpd-filter-phase-configuration-value record)]) + (string-append "filter " + "\"" name "\" " + "phase " phase " " + "match " + (apply string-append ; turn the options into a string + (flatten + (map (lambda (option) + (opensmtpd-option-configuration->string option #:space-after-! #f)) + options))) + " " + decision " " + (if (string-in-list? decision (list "reject" "disconnect")) + (string-append "\"" message "\"") + "") + (if (string=? "rewrite" decision) + (string-append "rewrite " (number->string value)) + "") + "\n"))) + +;; filters elements may be , , +;; and lists that look like (list (opensmtpd-filter-configuration...) (opensmtpd-filter-phase-configuration ...) +;; ...) +;; this function converts it to a string. +;; Consider if a user passed in a valid , whose total valid filters +;; so that (get-opensmtpd-filters (opensmtpd-configuration)) returns +;; look like this: (we will call this list "total filters"): +;; (list (opensmtpd-filter +;; (name "rspamd") +;; (proc "rspamd")) +;; (list (opensmtpd-filter-phase-configuration ; this is a listen-on, with a filter-chain. +;; (name "dkimsign") +;; ...) +;; (opensmtpd-filter +;; (name "rspamd") +;; (proc "rspamd")))) +;; +;; did you notice that filter "rspamd" is listed twice? How do you make sure that it is NOT +;; printed twice in smtpd.conf? +;; 1st flatten "total filters", then remove its duplicates. Then print all of those filters. +;; 2nd now we go through "total filters", and we only print the non-filter-chains. +(define (opensmtpd-filters->string filters) + ;; first display the unique s. and s. + ;; to do this: flatten filters, then remove duplicates. + (string-append + (apply string-append + (map (lambda (filter) + (cond ((opensmtpd-filter-phase-configuration? filter) + (opensmtpd-filter-phase-configuration->string filter)) + (else ; you are a + (string-append "filter " + "\"" (opensmtpd-filter-configuration-name filter) "\" " + (if (opensmtpd-filter-exec filter) + "proc-exec " + "proc ") + "\"" (opensmtpd-filter-configuration-proc filter) "\"" + "\n")))) + (delete-duplicates (flatten filters)))) + ;; now we have to print the filter chains. + (apply string-append + (remove boolean? + (map (lambda (filter) + (cond ((list? filter) + (opensmtpd-filter-chain->string filter)) + (else ; you are a + #f))) + filters))))) + +(define (opensmtpd-configuration-listen->string string) + (string-append + "include \"" string "\"\n")) + +(define (opensmtpd-configuration-srs->string record) + (let ([key (opensmtpd-srs-configuration-key record)] + [backup-key (opensmtpd-srs-configuration-backup-key record)] + [ttl-delay (opensmtpd-srs-configuration-ttl-delay record)]) + (string-append + (variable->string key #:append "srs key " #:postpend "\n") + (variable->string backup-key #:append "srs key backup " #:postpend "\n") + (variable->string ttl-delay #:append "srs ttl " #:postpend "\n") + "\n"))) + +;; TODO make sure all options here work! I just fixed limit-max-rcpt! +(define (opensmtpd-smtp-configuration->string record) + (let ([ciphers (opensmtpd-smtp-configuration-ciphers record)] + [limit-max-mails (opensmtpd-smtp-configuration-limit-max-mails record)] + [limit-max-rcpt (opensmtpd-smtp-configuration-limit-max-rcpt record)] + [max-message-size (opensmtpd-smtp-configuration-max-message-size record)] + [sub-addr-delim (opensmtpd-smtp-configuration-sub-addr-delim record)]) + (string-append + (variable->string ciphers #:append "smtp ciphers " #:postpend "\n") + (variable->string limit-max-mails #:append "smtp limit max-mails " #:postpend "\n") + (variable->string limit-max-rcpt #:append "smtp limit max-rcpt " #:postpend "\n") + (variable->string max-message-size #:append "smtp max-message-size " #:postpend "\n") + (variable->string sub-addr-delim #:append "smtp sub-addr-delim " #:postpend "\n") + "\n"))) + +(define (opensmtpd-configuration-queue->string record) + (let ([compression (opensmtpd-queue-configuration-compression record)] + [encryption (opensmtpd-queue-configuration-encryption record)] + [ttl-delay (opensmtpd-queue-configuration-ttl-delay record)]) + (string-append + (if compression + "queue compression\n" + "") + (if encryption + (string-append + "queue encryption " + (if (not (boolean? encryption)) + encryption + "") + "\n") + "") + (if ttl-delay + (string-append "queue ttl" ttl-delay "\n") + "")))) + +;; build a list of from +;; opensmtpd-configuration-matches, which is a list of . +;; Each has a fieldname 'action', which accepts an . +(define (get-opensmtpd-actions record) + (define opensmtpd-actions + (let loop ([list (opensmtpd-configuration-matches record)]) + (if (null? list) + '() + (cons (opensmtpd-match-configuration-action (car list)) + (loop (cdr list)))))) + (delete-duplicates (append opensmtpd-actions))) + +;; build a list of opensmtpd-pki-configurations from +;; opensmtpd-configuration-listen-ons and +;; get-opensmtpd-actions +(define (get-opensmtpd-pki-configurations record) + ;; TODO/FIXME/maybe/wishlist could get-opensmtpd-actions -> NOT have an opensmtpd-action-relay-configuration? + ;; I think so. And if it did NOT have a relay configuration, then action-pkis would be '() when + ;; it needs to be #f. because if the opensmtpd-configuration has NO pkis, then this function will + ;; return '(), when it should return #f. If it returns '(), then opensmtpd-configuration-fieldname->string will + ;; print the string "\n" instead of "" + (define action-pkis + (let loop1 ([list (get-opensmtpd-actions record)]) + (if (null? list) + '() + (if (and (opensmtpd-action-relay-configuration? (car list)) + (opensmtpd-action-relay-configuration-pki (car list))) + (cons (opensmtpd-action-relay-configuration-pki (car list)) + (loop1 (cdr list))) + (loop1 (cdr list)))))) + ;; FIXME/TODO/maybe/wishlist + ;; this could be #f aka left blank. aka there are no listen-ons records with pkis. + ;; aka there are no lines in the configuration like: + ;; listen on eth0 tls pki smtp.gnucode.me in that case the smtpd.conf will have an extra "\n" + (define listen-on-pkis + (let loop2 ([list (opensmtpd-configuration-listen-ons record)]) + (if (null? list) + '() + (if (opensmtpd-listen-on-configuration-pki (car list)) + (cons (opensmtpd-listen-on-configuration-pki (car list)) + (loop2 (cdr list))) + (loop2 (cdr list)))))) + (delete-duplicates (append action-pkis listen-on-pkis))) + +;; takes in a and returns a list whose elements are , +;; , and a filter-chain. +;; It returns a list of and/or +;; here's an example of what this procedure might return: +;; (list (opensmtpd-filter-configuration...) (opensmtpd-filter-phase-configuration ...) +;; (openmstpd-filter ...) (opensmtpd-filter-phase-configuration ...) +;; ;; this next list is a filter-chain. +;; (list (opensmtpd-filter-phase-configuration ...) (opensmtpd-filter-configuration...))) +;; +;; This procedure handles filter chains a little odd. +(define (get-opensmtpd-filters record) + (define list-of-listen-on-records (if (opensmtpd-configuration-listen-ons record) + (opensmtpd-configuration-listen-ons record) + '())) + + (define listen-on-socket-filters + (if (opensmtpd-listen-on-socket-configuration-configuration-filters (opensmtpd-configuration-listen-on-socket record)) + (opensmtpd-listen-on-socket-configuration-configuration-filters (opensmtpd-configuration-listen-on-socket record)) + '())) + + (delete-duplicates + (append (remove boolean? + (map-in-order (lambda (listen-on-record) ; get the filters found in the s + (if (and (opensmtpd-listen-on-configuration-filters listen-on-record) + (= 1 (length (opensmtpd-listen-on-configuration-filters + listen-on-record)))) + (car (opensmtpd-listen-on-configuration-filters listen-on-record)) + (opensmtpd-listen-on-configuration-filters listen-on-record))) + list-of-listen-on-records)) + listen-on-socket-filters))) + +(define (flatten . lst) + "Return a list that recursively concatenates all sub-lists of LST." + (define (flatten1 head out) + (if (list? head) + (fold-right flatten1 out head) + (cons head out))) + (fold-right flatten1 '() lst)) + +;; This function takes in a record, or list, or anything, and returns +;; a list of s assuming the thing you passed into it had +;; any s. +;; +;; is object record? call func on it's fieldnames +;; is object list? loop through it's fieldnames calling func on it's records +;; is object #f or string? or '()? -> #f +(define (get-opensmtpd-tables value) + (delete-duplicates + (remove boolean? (flatten ;; turn (list '(1) '(2 '(3))) -> '(1 2 3) + (cond ((opensmtpd-table-configuration? value) + value) + ((record? value) + (let* ([record-type (record-type-descriptor value)] + [list-of-record-fieldnames (record-type-fields record-type)]) + (map (lambda (fieldname) + (get-opensmtpd-tables ((record-accessor record-type fieldname) value))) + list-of-record-fieldnames))) + ((and (list? value) (not (null? value))) + (map get-opensmtpd-tables value)) + (else #f)))))) + +(define (opensmtpd-configuration-fieldname->string record fieldname-accessor record->string) + (if (fieldname-accessor record) + (begin + (string-append + (list-of-records->string (fieldname-accessor record) record->string) "\n")) + "")) + +(define (list-of-records->string list-of-records record->string) + (string-append + (cond [(not (list? list-of-records)) + (record->string list-of-records)] + [else + (let loop ([list list-of-records]) + (if (null? list) + "" + (string-append + (record->string (car list)) + (loop (cdr list)))))]))) + + +;; FIXME/TODO should I use format here srfi-28 ? +;; web.scm nginx does a (format #f "string" "another string") +;; this could be a list like (list (file-append opensmtpd-dkimsign "/libexec/filter") "-d gnucode.me -s /path/to/selector.cert") +;; Then opensmtpd-configuration->mixed-text-file could be rewritten to be something like +;; (mixed-text-file (eval `(string-append (opensmtpd-configuration-fieldname->string ...)) (gnu services mail))) +(define (opensmtpd-configuration->mixed-text-file record) + ;; should I use this named let, or should I give this a name, or not use it at all... + ;; eg: (write-all-fieldnames (list (cons fieldname fieldname->string) (cons fieldname2 fieldname->string))) + ;; (let loop ([list (list (cons opensmtpd-configuration-includes (lambda (string) + ;; (string-append + ;; "include \"" string "\"\n"))) + ;; (cons opensmtpd-configuration-smtp opensmtpd-smtp->string) + ;; (cons opensmtpd-configuration-srs opensmtpd-srs->string))]) + ;; (if (null? list) + ;; "" + ;; (string-append (opensmtpd-configuration-fieldname->string record + ;; (caar list) + ;; (cdar list)) + ;; (loop (cdr list))))) + + ;;(mixed-text-file "opensmtpd.conf") + (string-append + ;; write out the includes + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-includes + opensmtpd-configuration-listen->string) + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-bounce + (lambda (%bounce) + (if %bounce + (list-of-strings->string %bounce) + ""))) + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-smtp + opensmtpd-smtp-configuration->string) + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-srs + opensmtpd-configuration-srs->string) + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-queue + opensmtpd-configuration-queue->string) + ;; write out the mta-max-deferred + (opensmtpd-configuration-fieldname->string + record opensmtpd-configuration-mta-max-deferred + (lambda (var) + (string-append "mta max-deferred " + (number->string (opensmtpd-configuration-mta-max-deferred record)) "\n"))) + ;;write out all the tables + (opensmtpd-configuration-fieldname->string record get-opensmtpd-tables opensmtpd-table-configuration->string) + ;; TODO should I change the below line of code into these two lines of code? + ;;(opensmtpd-configuration-fieldname->string record get-opensmtpd-filters-and-filter-phases opensmtpd-filter-and-filter-phase->string) + ;;(opensmtpd-configuration-fieldname->string record get-opensmtpd-filter-chains opensmtpd-filter-chain->string) + ;; write out all the filters + (opensmtpd-filters->string (get-opensmtpd-filters record)) + ;; write out all the cas + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-cas opensmtpd-ca-configuration->string) + ;; write out all the pkis + (opensmtpd-configuration-fieldname->string record get-opensmtpd-pki-configurations opensmtpd-pki-configuration->string) + ;; write all of the listen-on-records + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-listen-ons + opensmtpd-listen-on-configuration->string) + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-listen-on-socket + opensmtpd-listen-on-socket-configuration->string) + ;; write all the actions + (opensmtpd-configuration-fieldname->string record get-opensmtpd-actions + opensmtpd-action->string) + ;; write all of the matches + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-matches opensmtpd-match-configuration->string))) + (define %default-opensmtpd-config-file (plain-file "smtpd.conf" " -- 2.36.1 From debbugs-submit-bounces@debbugs.gnu.org Fri Jun 17 17:54:32 2022 Received: (at 56046) by debbugs.gnu.org; 17 Jun 2022 21:54:32 +0000 Received: from localhost ([127.0.0.1]:47131 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o2JvE-0002Gh-CT for submit@debbugs.gnu.org; Fri, 17 Jun 2022 17:54:32 -0400 Received: from mx1.dismail.de ([78.46.223.134]:21709) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o2JvC-0002GT-Gv for 56046@debbugs.gnu.org; Fri, 17 Jun 2022 17:54:31 -0400 Received: from mx1.dismail.de (localhost [127.0.0.1]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 6e36d91a for <56046@debbugs.gnu.org>; Fri, 17 Jun 2022 23:54:23 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; d=dismail.de; h=from:to:cc :subject:date:message-id:mime-version:content-type :content-transfer-encoding; s=20190914; bh=q2Ce8q6uAxJu4Oen6Bhg4 +NbiTB2Phd+AjQxSfHsI3I=; b=DA2lvnFTF/3ht7A4bZpJqFMFSitWzwzlI3B81 iPlTnzmYO1gl0dlTpbcAhtM2SJL1AwI/Zcwzms6tBP3Q58azxDh8oJ0082t/1fcR QXT7ORoAFKCOERnj6Wkp/5ib5Z7oTY8qEh69sf4FQN8ywAew57wGq7RxeRieALrf MFx0ZFM+ew3lk59FyT/RlRLKbueRnGFNsDHLu25Wg7rw3WIIng1fZY9dcBSOcVA4 ddm9R+C6j2RVeJW7AmVfs0H2fF4i+JL4j5A/Pb6eKKnZa9erDT7bRz/76SoFLYLD MZKe1kYsprR+CXm3HWt4S8rY5n4t48ZHQz26LFtyDqmO1D/dw== Received: from smtp2.dismail.de ( [10.240.26.12]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 779a86ac for <56046@debbugs.gnu.org>; Fri, 17 Jun 2022 23:54:22 +0200 (CEST) Received: from smtp2.dismail.de (localhost [127.0.0.1]) by smtp2.dismail.de (OpenSMTPD) with ESMTP id caf993c3 for <56046@debbugs.gnu.org>; Fri, 17 Jun 2022 23:54:22 +0200 (CEST) Received: by dismail.de (OpenSMTPD) with ESMTPSA id c1362d04 (TLSv1.3:AEAD-AES256-GCM-SHA384:256:NO); Fri, 17 Jun 2022 23:54:18 +0200 (CEST) From: Joshua Branson To: 56046@debbugs.gnu.org Subject: [PATCH] gnu: services: opensmtpd-records-task-list.org: Some notes about how I thought about building this service. And some additional task lists, as well as the WIP documentation. Date: Fri, 17 Jun 2022 17:54:07 -0400 Message-Id: <20220617215407.21290-1-jbranso@dismail.de> X-Mailer: git-send-email 2.36.1 MIME-Version: 1.0 Content-Type: text/plain; charset=y Content-Transfer-Encoding: 8bit X-Debbugs-Envelope-To: 56046 Cc: Joshua Branson X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" --- gnu/services/opensmtpd-records-task-list.org | 5122 ++++++++++++++++++ 1 file changed, 5122 insertions(+) create mode 100644 gnu/services/opensmtpd-records-task-list.org diff --git a/gnu/services/opensmtpd-records-task-list.org b/gnu/services/opensmtpd-records-task-list.org new file mode 100644 index 0000000000..c138aab8fe --- /dev/null +++ b/gnu/services/opensmtpd-records-task-list.org @@ -0,0 +1,5122 @@ +#+title: Opensmtpd Records Task List +#+AUTHOR: Joshua Branson + + +(service (@ (gnu services mail) opensmtpd-service-type) + ((@ (gnu services mail) opensmtpd-configuration) + (config-file …))) + +* tasks +** PROJ I have decent data structures. now let's get some good code. [0/6] +*** why are good data structures important? +**** nckx's advice: use a simple 1-1 mapping +"...as I think Guix services ought to faithfully wrap the native +syntax whenever possible (implement alternative simple APIs on top of +that — fine)." + +-nckx from irc on #guix + +**** To follow nckx's advice, one might create the == like this: +#+BEGIN_SRC scheme + (service opensmtpd-service + (opensmtpd-configuration + (includes ...) + (tables ...) + (pkis ...) + (filters ...) + (listen-on ...) + (actions ...) + (matches ...))) +#+END_SRC + +Defining the service this way, makes it VERY easy from a development point of +view. But it makes it possible for users to create simple mistakes when +defining the service. + +For example, it is possible to define an nginx service that will successfully +reconfigure the system. BUT after reboot nginx refuses to start. Why? Who knows. +Guix won't tell you. Neither will the Shepherd. To fix this, the user has to go +digging into the nginx logs, and he might not know where to find those. If +possible, when the user specificies a == that has +obvious errors, then the guix services should make reconfigure fail and print a +helpful error message. + +**** BUT it would be better if the service uses better datastructures. + +I should follow nckx's advice, and Linus' advice: good programmers use good +datastructures. If you have good datastructures, then your code will almost +write itself. + +It might make the service a little harder to develop, but end-users will find +the service easier to use. This would eliminate common errors like misspellings +and give appropriate error messages. Practically it would ensure each +== has a corresponding ==, +creating a table name and then misspelling the table name later, and defining +a table but never using it, etc. + +**** Example configuration + +#+BEGIN_SRC scheme +(service opensmtpd-service-type + (let ([interface "lo"] + [creds-table (opensmtpd-table-configuration + (name "creds") + (data + (list + (cons "joshua" + "$6$Ec4m8FgKjT2F/03Y$k66ABdse9TzCX6qaALB3WBL9GC1rmAWJmaoSjFMpbhzat7DOpFqpnOwpbZ34wwsQYIK8RQlqwM1I/v6vsRq86."))))] + [receive-action (opensmtpd-action-local-delivery-configuration + (name "receive") + (method (opensmtpd-maildir-configuration + (pathname "/home/%{rcpt.user}/Maildir") + (junk #t))) + (virtual (opensmtpd-table-configuration + (name "virtual") + (data (list "josh" "jbranso@dismail.de")))))] + [filter-dkimsign (opensmtpd-filter-configuration + (name "dkimsign") + (exec #t) + (proc (string-append "/path/to/dkimsign -d gnucode.me -s 2021-09-22 -c relaxed/relaxed -k " + "/path/to/dkimsign-key user nobody group nobody")))] + [smtp.gnucode.me (opensmtpd-pki-configuration + (domain "smtp.gnucode.me") + (cert "opensmtpd.scm") + (key "opensmtpd.scm"))]) + (opensmtpd-configuration + (mta-max-deferred 50) + (queue + (opensmtpd-queue-configuration + (compression #t))) + (smtp + (opensmtpd-smtp-configuration + (max-message-size "10M"))) + (srs + (opensmtpd-srs-configuration + (ttl-delay "5d"))) + (listen-ons + (list + (opensmtpd-listen-on-configuration + (interface interface) + (port 25) + (secure-connection "tls") + (filters (list (opensmtpd-filter-phase-configuration + (name "noFRDNS") + (phase "commit") + (options (list (opensmtpd-option-configuration + (option "fcrdns") + (not #t)))) + (decision "disconnect") + (message "433 No FCRDNS")))) + (pki smtp.gnucode.me)) + ;; this lets local users logged into the system via ssh send email + (opensmtpd-listen-on-configuration + (interface interface) + (port 465) + (secure-connection "smtps") + (pki smtp.gnucode.me) + (auth creds-table) + (filters (list filter-dkimsign))) + (opensmtpd-listen-on-configuration + (interface interface) + (port 587) + (secure-connection "tls-require") + (pki smtp.gnucode.me) + (auth creds-table) + (filters (list filter-dkimsign))))) + (matches (list + (opensmtpd-match-configuration + (action (opensmtpd-action-relay-configuration + (name "relay"))) + (options (list (opensmtpd-option-configuration + (option "for any")) + (opensmtpd-option-configuration + (option "from any")) + (opensmtpd-option-configuration + (option "auth"))))) + (opensmtpd-match-configuration + (action receive-action) + (options (list (opensmtpd-option-configuration + (option "from any")) + (opensmtpd-option-configuration + (option "for domain") + (data (opensmtpd-table-configuration + (name "domain-table") + (data (list "gnucode.me" "gnu-hurd.com")))))))) + (opensmtpd-match-configuration + (action receive-action) + (options (list (opensmtpd-option-configuration + (option "for local")))))))))) +#+END_SRC + +:OldConfigurationSyntax: +#+BEGIN_SRC scheme + (service opensmtpd-service-type + (opensmtpd-configuration + (pkis (list + (opensmtpd-pki-configuration + ...))) + (tables (list + (opensmtpd-table-configuration + ...) + (opensmtpd-table-configuration + ...))) + (listen-ons + (list + (opensmtpd-listen-on-configuration + ...) + (opensmtpd-listen-on-configuration + ...))) + (actions + (list + (opensmtpd-action + ...) + (opensmtpd-action + ...))) + (matches (list + (opensmtpd-match-configuration + ...) + (opensmtpd-match-configuration + ...))) + (filter-chains + (list + (opensmtpd-filter-chain + (name "dropDumbEmails") + (filter-names (list "nofcrdnsDisconnect" + "nordnsDisconnect"))))) + (filter-phases + (list (opensmtpd-filter-phase-configuration + ...) + (opensmtpd-filter-phase-configuration + ...))))) +#+END_SRC + +Here you have to define the =pki=s twice! You define it once in the =pkis= +section, and then you reference it later. This could potentially cause a +mispelling error. That would be silly to debug as an end-user. + +:END: + +*** PROJ tweek the code for == & == records [4/7] +**** Why I chose the current datastructures of == & == + +According to the man page I have a four kinds of filters: + +#+BEGIN_EXAMPLE +1. filter chain-name chain {filter-name [, ...]} + Register a chain of filters chain-name, consisting of the filters listed from filter-name. + Filters part of a filter chain are executed in order of declaration for each phase that + they are registered for. A filter chain may be used in place of a filter for any direc‐ + tive but filter chains themselves. +2. filter filter-name phase phase-name match conditions decision + Register a filter filter-name. A decision about what to do with the mail is taken at + phase phase-name when matching conditions. Phases, matching conditions, and decisions are + described in MAIL FILTERING, below. +3. filter filter-name proc proc-name + Register "proc" filter filter-name backed by the proc-name process. +4. filter filter-name proc-exec command + Register and execute "proc" filter filter-name from command. If command starts with a + slash it is executed with an absolute path, else it will be run from + “/gnu/store/2d13sdz76ldq8zgwv4wif0zx7hkr3mh2-opensmtpd-6.8.0p2/libexec/opensmtpd”. +#+END_EXAMPLE + +=chain-name= could be easily represented as a list of filters. in the +opensmtpd-configuration-filter fieldname: + +#+BEGIN_SRC scheme +(opensmtpd-configuration + (listen-on + (filter + (list (opensmtpd-filter-configuration) + (opensmtpd-filter-configuration) + (opensmtpd-filter-configuration))))) +#+END_SRC + +For example, this is probably easier + +#+BEGIN_SRC scheme + (opensmtpd-configuration + (actions (list + (opensmtpd-action + (name "relay") + (method (opensmtpd-relay-configuration + (domain (opensmtpd-table-configuration + ;;(name "domains") ;; with some smart coding, the name would NOT be needed. + (data (list + "gnucode.me" + "gnu-hurd.com")))))))))) +#+END_SRC + +than the alternative: + +#+BEGIN_SRC scheme + (opensmtpd-configuration + (tables (list + (opensmtpd-table-configuration + (name "domains") + (data (list + "gnucode.me" + "gnu-hurd.com"))))) + (actions (list + (opensmtpd-action + (name "relay") + (method (opensmtpd-relay-configuration + (domain "domains"))))))) +#+END_SRC + +**** some example code for each of the 3 types of filters + +1. filter phase +#+BEGIN_SRC scheme +(opensmtpd-filter-phase-configuration + (name "phase") + (phase "connect") + (options + (list + (opensmtpd-option-configuration + (option "src") + (not #t) + (regex #t) + (table (opensmtpd-table-configuration (name "src-option-table") + (data (list "cat" "hot"))))))) + (decision "reject") + (message "We do not want spam here!")) +#+END_SRC + +#+RESULTS: + +2. filter proc +this is a filter-proc +#+BEGIN_SRC scheme +(opensmtpd-filter + (name "proc") + (proc "dkimsign")) +#+END_SRC + +3. filter proc-exec +#+BEGIN_SRC scheme +(opensmtpd-filter + (name "proc") + (exec #t) + (proc "dkimsign")) +#+END_SRC + +***** Why am I doing the data structure like the above? + +filter-proc and proc-exec as defined in man smtpd.conf can both use the same + record. That works just fine. + +But filter-phase is a different beast. I do NOT want someone to accidentally +define something like the following which is BAD data: + +#+BEGIN_SRC scheme +(opensmtpd-filter + (name "proc") + (exec #t) + (proc "dkimsign")) + +#+END_SRC +**** NO Is it advantageous/desireable to merge == & + +When a user creates a filter, he is either going to create a +=~ or an ~= NOT both. If +we define separate records, then it is impossible for a user to accidentally +define a filter record using fieldnames from both filter types. eg: + +#+BEGIN_SRC scheme +(opensmtpd-filter-configuration + (name "filter") + (exec #t) + (proc "dkimsign") + (phase "connect")) ;; this phase should NOT be there. this is a +#+END_SRC + +If == & == are separte then the above +would correctly result in an error message for free. +**** TODO make fieldname 'proc' accept a list of strings and/or a s + +Suppose you want to do dkimsigning in smtpd.conf. Here is how you might +register the official opensmtpd dkimsign filter: + +#+BEGIN_EXAMPLE +filter "dkimsign" proc-exec "filter-dkimsign -d -s \ + -k /gnu/store/2d13sdz76ldq8zgwv4wif0zx7hkr3mh2-opensmtpd-6.8.0p2/etc/dkim/private.key" user _dkimsign group _dkimsign +#+END_EXAMPLE + +For example my hacky code to do dkimsigning looks like: + +#+BEGIN_SRC scheme +filter \"dkimsign\" \ + proc-exec \"" path-to-filter-dkimsign " -d gnucode.me -s 2021-09-22 -c relaxed/relaxed -k " etc-dkimsign-key-file + " \" \ + user nobody group nogroup + +#+END_SRC + +Here is some example code of how we could create an +== that registers a dkimsign filter. The code +below probably will NOT work. + +#+BEGIN_SRC scheme +(let ((etc-dkimsign-key-file "filename.key") + (path-to-dkimsign-key "/etc/opensmtpd/"))) +(opensmtpd-filter-configuration + (name "dkimsign") + (proc (list + (file-append opensmtpd-filter-dkimsign "/libexec/opensmtpd/filter-dkimsign") + " -d gnucode.me -s 2021-09-22 -c relaxed/relaxed -k " + ~#(let ([UID-nobody (passwd:uid (getpw "nobody"))] + [GID-root (group:gid (getgr "root"))] + [GID-nogroup (group:gid (getgr "nogroup"))]) + ;; #o550 user root can read/open the directory + ;; and the group "root" can read/open the directory. + ;; change these two lines to (mkdir-p) ? + (unless (file-exists? "/etc/opensmtpd") + (mkdir "/etc/opensmtpd" #o550)) + + ;; root can read/write/execute on directory dkimsign + ;; group "root" can read and execute + (unless (file-exists? "/etc/opensmtpd/dkimsign") + (mkdir "/etc/opensmtpd/dkimsign" #o750)) + + (copy-file path-to-dkimsign-key etc-dkimsign-key-file) + ;; make the etc-dkimsign-key-file to owned by nobody and group nogroup. + (chown "/etc/opensmtpd" UID-nobody GID-root) + (chown "/etc/opensmtpd/dkimsign" UID-nobody GID-root) + (chown etc-dkimsign-key-file UID-nobody GID-nogroup) + "/etc/opensmtpd/dkimsign/2021-09-22-rsa1024-gnucode.me.key") + "user nobody group nogroup")) + (exec #)) +#+END_SRC + +Here is the full for how I currently run opensmtpd: + +#+BEGIN_SRC sh :dir ~/prog/gnu/guix/guix-config/linode-guix-system-configuration/ :results raw +cat opensmtpd.scm +#+END_SRC + +#+RESULTS: +#+BEGIN_SRC scheme +(define-module (opensmtpd) + #:use-module (guix gexp) + #:use-module (guix records) + #:use-module (gnu packages mail) ;;for finding location of filter-dkimsign + #:export ( + %smtpd.conf + )) + + +;; to create credentials for now, I need to do the following: +;; find /gnu/store -name '*encrypt*' | grep opensmtpd +;; /gnu/store/blah/opensmtpd/encrypt +(define creds + (plain-file "creds" + ;; this is my joshua's password for server. This can be found on dobby's /home/joshua/.authinfo/ + "joshua $6$Ec4m8FgKjT2F/03Y$k66ABdse9TzCX6qaALB3WBL9GC1rmAWJmaoSjFMpbhzat7DOpFqpnOwpbZ34wwsQYIK8RQlqwM1I/v6vsRq86.")) + +(define vdoms + (plain-file + "vdoms" + "gnucode.me +gnu-hurd.com")) + +(define vusers + (plain-file + "vusers" + "joshua@gnucode.me joshua +jbranso@gnucode.me joshua +postmaster@gnucode.me joshua")) + +(define path-to-filter-dkimsign + (file-append opensmtpd-filter-dkimsign "/libexec/opensmtpd/filter-dkimsign")) + +(define path-to-dkimsign-key (string-append (getcwd) "/email-dkim-ssh-keys/2021-09-22-rsa1024-gnucode.me.key")) +(define etc-dkimsign-key-file "/etc/opensmtpd/dkimsign/2021-09-22-rsa1024-gnucode.me.key") + +;; FIXME: This should become a derivation. Currently it just runs when I evaluate +;; %smtpd.conf. For example it should look like this? +;; (define build-exp +;; #~(begin +;; (mkdir #$output) +;; (chdir #$output) +;; (symlink (string-append #$coreutils "/bin/ls") +;; "list-files"))) + +;; I will need to extend the opensmtpd service, to create a directory +;; in etc. This line needs to be added to etc-service. +;; (service-extension etc-service-type opensmtpd-etc-service) +;; I'll then need to create a opensmtpd-etc-service procedure. ganeti has +;; a good example. + +;; It should also use the /etc service, which is a service for creating +;; directories and files in /etc ? +(define (create-etc-dkimsign-key-file) + #~(let ([UID-nobody (passwd:uid (getpw "nobody"))] + [GID-root (group:gid (getgr "root"))] + [GID-nogroup (group:gid (getgr "nogroup"))]) + ;; #o550 user root can read/open the directory + ;; and the group "root" can read/open the directory. + ;; change these two lines to (mkdir-p) ? + (unless (file-exists? "/etc/opensmtpd") + (mkdir "/etc/opensmtpd" #o550)) + + ;; root can read/write/execute on directory dkimsign + ;; group "root" can read and execute + (unless (file-exists? "/etc/opensmtpd/dkimsign") + (mkdir "/etc/opensmtpd/dkimsign" #o750)) + + (copy-file path-to-dkimsign-key etc-dkimsign-key-file) + ;; ;; ;; make the etc-dkimsign-key-file to owned by nobody and group nogroup. + (chown "/etc/opensmtpd" UID-nobody GID-root) + (chown "/etc/opensmtpd/dkimsign" UID-nobody GID-root) + (chown etc-dkimsign-key-file UID-nobody GID-nogroup) + etc-dkimsign-key-file)) + +(define %smtpd.conf + (mixed-text-file "smtpd.conf" + " +# This is the smtpd server system-wide configuration file. +# See smtpd.conf(5) for more information. +# borrowed from the archlinux guix +# https://wiki.archlinux.org/index.php/OpenSMTPD#Simple_OpenSMTPD/mbox_configuration + +# My TLS certificate and key +table aliases file:/etc/aliases +pki smtp.gnucode.me cert \"/etc/letsencrypt/live/gnucode.me/fullchain.pem\" +pki smtp.gnucode.me key \"/etc/letsencrypt/live/gnucode.me/privkey.pem\" + +# for now I am NOT using the virtual credentials +# table creds { joshua = $6$Ec4m8FgKjT2F/03Y$k66ABdse9TzCX6qaALB3WBL9GC1rmAWJmaoSjFMpbhzat7DOpFqpnOwpbZ34wwsQYIK8RQlqwM1I/v6vsRq86. } +table creds \"" creds "\" +table vdoms \"" vdoms "\" +# table vdoms { gnucode.me, gnu-hurd.com } +# table vusers { joshua@gnucode.me = joshua, jbranso@gnucode.me = joshua, postmaster@gnucode.me = joshua } +table vusers \"" vusers "\" + +# this totally works! run this as user nobody! +# info about dkimsign ...ing +# https://openports.pl/path/mail/opensmtpd-filters/dkimsign +# sudo -u nobody /gnu/store/g17vdv4l03bacn7qbdpb5v8l8vgdxcld-opensmtpd-filter-dkimsign-0.5/libexec/opensmtpd/filter-dkimsign -d gnucode.me -s 2020 -c relaxed/relaxed -k etc-dkimsign-key-file /home/joshua/linode-guix-system-configuration/email-dkim-ssh-keys/20201004-gnucode.me.key user nobody group nogroup + +filter \"dkimsign\" \ + proc-exec \"" path-to-filter-dkimsign " -d gnucode.me -s 2021-09-22 -c relaxed/relaxed -k " etc-dkimsign-key-file ;;(create-etc-dkimsign-key-file) + " \" \ + user nobody group nogroup + +# port 25 is used only for receiving from external servers, and they may start a +# TLS session if the want. +listen on eth0 port 25 tls pki smtp.gnucode.me + +# For sending messages from outside of this server, you need to authenticate and use +# TLS +listen on eth0 port 465 smtps pki smtp.gnucode.me auth filter \"dkimsign\" +listen on eth0 port 587 tls-require pki smtp.gnucode.me auth filter \"dkimsign\" + +# users logged-in/ssh-ed into the system can send email +listen on lo port 25 tls pki smtp.gnucode.me + +# receive email action +action \"receive\" maildir \"/home/%{rcpt.user}/Maildir\" junk virtual +# action send the email to the world +action \"send\" relay + +# We accept to send email from any mail from authenticated users +match for any from any auth action \"send\" + +#finally we receive any incoming email +# maybe the next \"from any\" should be changed to \"for rdns\". +match from any for domain action \"receive\" +match for local action \"receive\"")) +(define-module (opensmtpd) + #:use-module (guix gexp) + #:use-module (guix records) + #:use-module (gnu packages mail) ;;for finding location of filter-dkimsign + #:export ( + %smtpd.conf + )) + + +;; to create credentials for now, I need to do the following: +;; find /gnu/store -name '*encrypt*' | grep opensmtpd +;; /gnu/store/blah/opensmtpd/encrypt +(define creds + (plain-file "creds" + ;; this is my joshua's password for server. This can be found on dobby's /home/joshua/.authinfo/ + "joshua $6$Ec4m8FgKjT2F/03Y$k66ABdse9TzCX6qaALB3WBL9GC1rmAWJmaoSjFMpbhzat7DOpFqpnOwpbZ34wwsQYIK8RQlqwM1I/v6vsRq86.")) + +(define vdoms + (plain-file + "vdoms" + "gnucode.me +gnu-hurd.com")) + +(define vusers + (plain-file + "vusers" + "joshua@gnucode.me joshua +jbranso@gnucode.me joshua +postmaster@gnucode.me joshua")) + +(define path-to-filter-dkimsign + (file-append opensmtpd-filter-dkimsign "/libexec/opensmtpd/filter-dkimsign")) + +(define path-to-dkimsign-key (string-append (getcwd) "/email-dkim-ssh-keys/2021-09-22-rsa1024-gnucode.me.key")) +(define etc-dkimsign-key-file "/etc/opensmtpd/dkimsign/2021-09-22-rsa1024-gnucode.me.key") + +;; FIXME: This should become a derivation. Currently it just runs when I evaluate +;; %smtpd.conf. For example it should look like this? +;; (define build-exp +;; #~(begin +;; (mkdir #$output) +;; (chdir #$output) +;; (symlink (string-append #$coreutils "/bin/ls") +;; "list-files"))) + +;; I will need to extend the opensmtpd service, to create a directory +;; in etc. This line needs to be added to etc-service. +;; (service-extension etc-service-type opensmtpd-etc-service) +;; I'll then need to create a opensmtpd-etc-service procedure. ganeti has +;; a good example. + +;; It should also use the /etc service, which is a service for creating +;; directories and files in /etc ? +(define (create-etc-dkimsign-key-file) + #~(let ([UID-nobody (passwd:uid (getpw "nobody"))] + [GID-root (group:gid (getgr "root"))] + [GID-nogroup (group:gid (getgr "nogroup"))]) + ;; #o550 user root can read/open the directory + ;; and the group "root" can read/open the directory. + ;; change these two lines to (mkdir-p) ? + (unless (file-exists? "/etc/opensmtpd") + (mkdir "/etc/opensmtpd" #o550)) + + ;; root can read/write/execute on directory dkimsign + ;; group "root" can read and execute + (unless (file-exists? "/etc/opensmtpd/dkimsign") + (mkdir "/etc/opensmtpd/dkimsign" #o750)) + + (copy-file path-to-dkimsign-key etc-dkimsign-key-file) + ;; ;; ;; make the etc-dkimsign-key-file to owned by nobody and group nogroup. + (chown "/etc/opensmtpd" UID-nobody GID-root) + (chown "/etc/opensmtpd/dkimsign" UID-nobody GID-root) + (chown etc-dkimsign-key-file UID-nobody GID-nogroup) + etc-dkimsign-key-file)) + +(define %smtpd.conf + (mixed-text-file "smtpd.conf" + " +# This is the smtpd server system-wide configuration file. +# See smtpd.conf(5) for more information. +# borrowed from the archlinux guix +# https://wiki.archlinux.org/index.php/OpenSMTPD#Simple_OpenSMTPD/mbox_configuration + +# My TLS certificate and key +table aliases file:/etc/aliases +pki smtp.gnucode.me cert \"/etc/letsencrypt/live/gnucode.me/fullchain.pem\" +pki smtp.gnucode.me key \"/etc/letsencrypt/live/gnucode.me/privkey.pem\" + +# for now I am NOT using the virtual credentials +# table creds { joshua = $6$Ec4m8FgKjT2F/03Y$k66ABdse9TzCX6qaALB3WBL9GC1rmAWJmaoSjFMpbhzat7DOpFqpnOwpbZ34wwsQYIK8RQlqwM1I/v6vsRq86. } +table creds \"" creds "\" +table vdoms \"" vdoms "\" +# table vdoms { gnucode.me, gnu-hurd.com } +# table vusers { joshua@gnucode.me = joshua, jbranso@gnucode.me = joshua, postmaster@gnucode.me = joshua } +table vusers \"" vusers "\" + +# this totally works! run this as user nobody! +# info about dkimsign ...ing +# https://openports.pl/path/mail/opensmtpd-filters/dkimsign +# sudo -u nobody /gnu/store/g17vdv4l03bacn7qbdpb5v8l8vgdxcld-opensmtpd-filter-dkimsign-0.5/libexec/opensmtpd/filter-dkimsign -d gnucode.me -s 2020 -c relaxed/relaxed -k etc-dkimsign-key-file /home/joshua/linode-guix-system-configuration/email-dkim-ssh-keys/20201004-gnucode.me.key user nobody group nogroup + +filter \"dkimsign\" \ + proc-exec \"" path-to-filter-dkimsign " -d gnucode.me -s 2021-09-22 -c relaxed/relaxed -k " etc-dkimsign-key-file ;;(create-etc-dkimsign-key-file) + " \" \ + user nobody group nogroup + +# port 25 is used only for receiving from external servers, and they may start a +# TLS session if the want. +listen on eth0 port 25 tls pki smtp.gnucode.me + +# For sending messages from outside of this server, you need to authenticate and use +# TLS +listen on eth0 port 465 smtps pki smtp.gnucode.me auth filter \"dkimsign\" +listen on eth0 port 587 tls-require pki smtp.gnucode.me auth filter \"dkimsign\" + +# users logged-in/ssh-ed into the system can send email +listen on lo port 25 tls pki smtp.gnucode.me + +# receive email action +action \"receive\" maildir \"/home/%{rcpt.user}/Maildir\" junk virtual +# action send the email to the world +action \"send\" relay + +# We accept to send email from any mail from authenticated users +match for any from any auth action \"send\" + +#finally we receive any incoming email +# maybe the next \"from any\" should be changed to \"for rdns\". +match from any for domain action \"receive\" +match for local action \"receive\"")) + +#+END_SRC +**** TODO what does rewrite needs value mean? Should it be a number? this is for == +from the documentation + +rewrite value the command parameter is rewritten with value +**** DONE sanitize the fieldname 'filters'. + +I can probably reuse existing code from the sanitize procedure found in +== fieldname 'filters'. +**** DONE write a get-opensmtpd-filters procedure + +This procedure takes all the values of fieldname 'filters' +and fieldname 'filters'. It returns a list of +, , and filter-chains, which is a list +of and . An example of what this +might return is: + +#+BEGIN_SRC scheme +(list (list (opensmtpd-filter)) + (list (opensmtpd-filter-phase-configuration)) + (list (opensmtpd-filter) ; this list is a filter-chain + (opensmtpd-filter-phase-configuration)) + (list (opensmtpd-filter-phase-configuration) ; this list is also a filter chain + (opensmtpd-filter) + (opensmtpd-filter))) +#+END_SRC + +These are some example bits of code that I can test my resulting code on. + +All unique filters of and . 4 of +them. One listen-on has no filters. +#+BEGIN_SRC scheme +(let ([interface "lo"] + [filter-dkimsign (opensmtpd-filter + (name "dkimsign") + (exec #t) + (proc (string-append "/path/to/dkimsign -d gnucode.me -s 2021-09-22 -c relaxed/relaxed -k " + "/path/to/dkimsign-key user nobody group nobody")))]) + (opensmtpd-configuration + (listen-on-socket + (opensmtpd-listen-on-socket-configuration-configuration + (filters (list (opensmtpd-filter + (name "rspamd") + (proc "rspamd")))))) + (listen-ons + (list + (opensmtpd-listen-on-configuration + (interface interface) + (filters (list (opensmtpd-filter-phase-configuration + (name "noFRDNS") + (phase "commit") + (options (list (opensmtpd-option-configuration + (option "fcrdns") + (not #t)))) + (decision "disconnect") + (message "No FCRDNS"))))) + ;; this lets local users logged into the system via ssh send email + (opensmtpd-listen-on + (interface interface) + (port 27) + (filters (list filter-dkimsign))) + (opensmtpd-listen-on-configuration + (port 29) + (interface interface)) + (opensmtpd-listen-on-configuration + (interface interface) + (filters (list (opensmtpd-filter + (name "rspamd") + (proc "rspamd") + (exec #t))))))))) +#+END_SRC + +4 unique filters. One of the filters is a filter chain. +#+BEGIN_SRC scheme +(let ([interface "lo"] + [filter-dkimsign (opensmtpd-filter + (name "dkimsign") + (exec #t) + (proc (string-append "/path/to/dkimsign -d gnucode.me -s 2021-09-22 -c relaxed/relaxed -k " + "/path/to/dkimsign-key user nobody group nobody")))]) + (opensmtpd-configuration + (listen-on-socket + (opensmtpd-listen-on-socket-configuration-configuration + (filters (list (opensmtpd-filter + (name "spamassassain") + (proc "spamassassain")) + (opensmtpd-filter-phase-configuration + (name "rdns") + (phase "data") + (options (list + (opensmtpd-option-configuration + (option "rdns") + (not #t)))) + (decision "reject") + (message "No RDNS")) + (opensmtpd-filter + (name "block") + (proc "block")) + (opensmtpd-filter-phase-configuration + (name "auth") + (phase "commit") + (options (list + (opensmtpd-option-configuration + (option "auth") + (regex #t) + (not #t) + (table (opensmtpd-table-configuration + (name "auth-table") + (data (list ".*@gmail.com" + ".*@dismail.de"))))))) + (decision "junk")))))) + (listen-ons + (list + (opensmtpd-listen-on-configuration + (interface interface) + (filters (list (opensmtpd-filter-phase-configuration + (name "noFRDNS") + (phase "commit") + (options (list (opensmtpd-option-configuration + (option "fcrdns") + (not #t)))) + (decision "disconnect") + (message "No FCRDNS"))))) + ;; this lets local users logged into the system via ssh send email + (opensmtpd-listen-on-configuration + (interface interface) + (port 27) + (filters (list filter-dkimsign))) + (opensmtpd-listen-on-configuration + (port 29) + (interface interface)) + (opensmtpd-listen-on-configuration + (interface interface) + (filters (list (opensmtpd-filter + (name "rspamd") + (proc "rspamd") + (exec #t))))))))) +#+END_SRC + +No filters at all. +#+BEGIN_SRC scheme +(get-opensmtpd-filters (opensmtpd-configuration)) ; no filters at all. +#+END_SRC + +This one prints rspamd twice! The get-opensmtpd-filters procedure returns a +duplicate filter. While get-opensmtpd-filters does return a duplicate filter. +#+BEGIN_SRC scheme +(opensmtpd-configuration + (listen-on-socket + (opensmtpd-listen-on-configuration-socket-configuration + (filters + (list (opensmtpd-filter + (name "rando") + (proc "rando")))))) + (listen-ons + (list + (opensmtpd-listen-on-configuration + (port 25) + (filters (list (opensmtpd-filter-phase-configuration + (name "noFRDNS") + (phase "commit") + (options (list (opensmtpd-option-configuration + (option "fcrdns") + (not #t)))) + (decision "disconnect") + (message "No FCRDNS")) + (opensmtpd-filter + (name "rspamd") + (proc "rspamd"))))) + (opensmtpd-listen-on-configuration + (port 465) + (filters (list + (opensmtpd-filter + (name "rspamd") + (proc "rspamd")) + (opensmtpd-filter + (name "block") + (proc "block"))))) + (opensmtpd-listen-on-configuration + (port 587)) + (opensmtpd-listen-on-configuration + (port 999) + (filters (list + (opensmtpd-filter + (name "bogofilter") + (proc "bogofilter")))))))) +#+END_SRC + +#+BEGIN_SRC scheme +(opensmtpd-configuration + (listen-ons + (list + (opensmtpd-listen-on-configuration + (filters (list (opensmtpd-filter + (name "bogofilter") + (proc "bogofilter"))))) + (opensmtpd-listen-on-configuration + (filters (list (opensmtpd-filter + (name "noFRDNS") + (proc "noFRDNS")) + (opensmtpd-filter + (name "rspamd") + (proc "rspamd")))))))) +#+END_SRC +**** PROJ by which method should I turn the filters in == into strings? [1/3] + +in (opensmtpd-configuration->mixed-text-file ) do either + +1. one line of code. faster, but violates the convention set by the other lines + of code around it. + #+BEGIN_SRC scheme + ;; write out all the filters + (opensmtpd-filters->string (get-opensmtpd-filters record)) + #+END_SRC + + This task in done: [[*(opensmtpd-filters->string (get-opensmtpd-filters record))][(opensmtpd-filters->string (get-opensmtpd-filters record))]] +2. two lines of code. slower, but follows the convention set by the other lines + of code around it. + #+BEGIN_SRC scheme + (opensmtpd-configuration-fieldname->string record get-opensmtpd-filters-and-filter-phases opensmtpd-filter-and-filter-phase->string) + (opensmtpd-configuration-fieldname->string record get-opensmtpd-filter-chains opensmtpd-filter-chain->string) + #+END_SRC + +***** DONE (opensmtpd-filters->string (get-opensmtpd-filters record)) + +Have one procedure that prints out all filters. + + #+BEGIN_SRC scheme +;; write out all the filters +(opensmtpd-filters->string (get-opensmtpd-filters record)) + #+END_SRC + +***** PROJ 4 procedures: get-filter-and-filter-phases, filter-and-filter-phases->string, get-filter-chains, filter-chains->string + +The bonus with this method is that I can add these two lines in +opensmtpd-configuration->mixed-text-file and keep a consistent coding framework: + +#+BEGIN_SRC scheme +;; write out all the filters and filter-phases +(opensmtpd-configuration-fieldname->string record get-opensmtpd-filters-and-filter-phases opensmtpd-filter-and-filter-phase->string) +;; write out all the filter chains +(opensmtpd-configuration-fieldname->string record get-opensmtpd-filter-chains opensmtpd-filter-chain->string) +#+END_SRC +**** DONE fix the sanitize procedure for == fieldnames 'phase-name', 'decision', etc. [5/5] +***** DONE sanitize == so that fieldname 'decision' option "reject" and "disconnect" requires a 'message'. +:LOGBOOK: +CLOCK: [2022-04-01 Fri 22:45]--[2022-04-02 Sat 04:13] => 5:28 +:END: + +This message must be RFC compliant. The message must start with 4xx or 5xx +status code. + +#+BEGIN_SRC scheme +(opensmtpd-listen-on-configuration + (filters (list + (opensmtpd-filter-phase-configuration + (name "junk") + (phase "connect") + (decision "junk") + (options + (list + (opensmtpd-option-configuration + (option "rdns"))))) + (opensmtpd-filter-phase-configuration + (name "src") + (phase "connect") + (options + (list + (opensmtpd-option-configuration + (option "src") + (data (opensmtpd-table-configuration + (name "src-table") + (data (list "cat" "hat"))))))) + (decision "reject"))))) +#+END_SRC + +#+RESULTS: + +#+BEGIN_EXAMPLE + fieldname: 'decision' options "disconnect" and "reject" require fieldname 'message' +to have a string. +ice-9/boot-9.scm:1685:16: In procedure raise-exception: +Throw to key `bad!' with args `(#< name: "src" phase: "connect" options: (#< option: "src" not: #f regex: #f table: #< name: "src-table" file-db: #f values: ("cat" "hat") type: #:5894:22 (x)>>>) decision: "reject" message: #f value: #f>)'. + +Entering a new prompt. Type `,bt' for a backtrace or `,q' to continue. +#+END_EXAMPLE + +#+BEGIN_SRC scheme +(opensmtpd-listen-on-configuration + (filters (list + (opensmtpd-filter-phase-configuration + (name "junk") + (phase "connect") + (decision "junk") + (options + (list + (opensmtpd-option-configuration + (option "rdns"))))) + (opensmtpd-filter-phase-configuration + (name "src") + (phase "connect") + (options + (list + (opensmtpd-option-configuration + (option "src") + (data (opensmtpd-table-configuration + (name "src-table") + (data (list "cat" "hat"))))))) + (decision "reject") + (message "322 Hello"))))) + fieldname: 'decision' options +"disconnect" and "reject" require fieldname 'message' to have a string. +The 'message' string must be RFC commpliant, which means that the string +must begin with a 4xx or 5xx status code. +ice-9/boot-9.scm:1685:16: In procedure raise-exception: +Throw to key `bad!' with args `(#< name: "src" phase: "connect" options: (#< option: "src" not: #f regex: #f data: #< name: "src-table" file-db: #f data: ("cat" "hat") type: #:1153:21 (x)>>>) decision: "reject" message: "322 Hello" value: #f>)'. + +Entering a new prompt. Type `,bt' for a backtrace or `,q' to continue. +#+END_SRC +***** DONE sanitize == so that fieldname 'decision' option "rewrite" requires a 'value'. +:LOGBOOK: +CLOCK: [2022-04-01 Fri 22:45]--[2022-04-02 Sat 04:13] => 5:28 +:END: + +#+BEGIN_SRC scheme +(opensmtpd-listen-on-configuration + (filters + (list + (opensmtpd-filter-phase-configuration + (name "noFRDNS") + (phase "commit") + (options (list (opensmtpd-option-configuration + (option "fcrdns") + (not #t)))) + (value #f) + (decision "rewrite")) ;; there needs to be a value here. rewrite requires a value! + ))) +$12 = #< interface: "lo" family: #f auth: #f auth-optional: #f filters: (#< name: "noFRDNS" phase: "commit" options: (#< option: "fcrdns" not: #t regex: #f data: #f>) decision: "rewrite" message: #f value: 343>) hostname: #f hostnames: #f mask-src: #f disable-dsn: #f pki: #f port: #f proxy-v2: #f received-auth: #f secure-connection: #f tag: #f> +scheme@(opensmtpd-records) [10]> (opensmtpd-listen-on-configuration + (filters + (list + (opensmtpd-filter-phase-configuration + (name "noFRDNS") + (phase "commit") + (options (list (opensmtpd-option-configuration + (option "fcrdns") + (not #t)))) + (decision "rewrite")) ;; there needs to be a value here. rewrite requires a value! + ))) + fieldname: 'decision' option +"rewrite" requires fieldname 'value' +to have a number. +ice-9/boot-9.scm:1685:16: In procedure raise-exception: +Throw to key `bad!' with args `(#< name: "noFRDNS" phase: "commit" options: (#< option: "fcrdns" not: #t regex: #f data: #f>) decision: "rewrite" message: #f value: #f>)'. + +Entering a new prompt. Type `,bt' for a backtrace or `,q' to continue. +#+END_SRC + +***** DONE sanitize == so that fieldname 'decision' option "junk" and "bypass" have no message or value + +#+BEGIN_SRC scheme +(opensmtpd-listen-on-configuration + (filters + (list + (opensmtpd-filter-phase-configuration + (name "noFRDNS") + (phase "commit") + (options (list (opensmtpd-option-configuration + (option "fcrdns") + ))) + (decision "junk") + (message "This is not a good email."))))) ; there should NOT be a message here! junk has no message. +#+END_SRC +***** DONE sanitize the options too. rdns requires a table for instance: + +#+BEGIN_EXAMPLE +At each phase, various conditions may be matched. The fcrdns, rdns, and src data are +available in all phases, but other data must have been already submitted before they are +available. + + fcrdns forward-confirmed reverse DNS is valid + rdns session has a reverse DNS + rdns
session has a reverse DNS in table + src
source address is in table + helo
helo name is in table + auth session is authenticated + auth
session username is in table + mail-from
sender address is in table + rcpt-to
recipient address is in table +#+END_EXAMPLE +***** DONE sanitize make sure that 'junking happens before phase 'committed'. +#+BEGIN_EXAMPLE + Descisions can be taken at any phase, though junking can only happen before a message is committed. +#+END_EXAMPLE + +#+BEGIN_SRC scheme +(opensmtpd-listen-on-configuration + (filters + (list + (opensmtpd-filter-phase-configuration + (name "junk-after-commit") + (phase "commit") + (options (list (opensmtpd-option-configuration + (option "fcrdns") + ))) + (decision "junk"))))) +#+END_SRC +*** PROJ make fieldnames that need a table accept a value of table [3/4] +**** DONE opensmtpd-action-local-delivery-configuration [3/3] +***** DONE alias == [2/2] +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 03:53] +:END: +****** DONE change the sanitize portion of the fieldname alias in the + +The below code does work in a REPL. +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +(opensmtpd-action-local-delivery-configuration + (alias + (opensmtpd-table-configuration + (name "My-table") + (data (list "gnu-hurd.com" "gnucode.me"))))) +#+END_SRC + +#+RESULTS: +****** DONE change relevant portions in opensmtpd-action-local-delivery-configuration->string +This bit of code works in the repl too! +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +((@@ (opensmtpd-records) opensmtpd-action-local-delivery-configuration->string) + (opensmtpd-action-local-delivery-configuration + (alias + (opensmtpd-table-configuration + (name "My-table") + (data (list "gnu-hurd.com" "gnucode.me")))))) +#+END_SRC + +***** DONE userbase == [2/2] +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 03:53] +:END: + +****** DONE change the sanitize portion of the fieldname userbase in the + +The below code does work in a REPL. +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) +(opensmtpd-action-local-delivery-configuration + (userbase + (opensmtpd-table-configuration + (name "this") + (data (list "job" "done"))))) +#+END_SRC + +#+RESULTS: +****** DONE change relevant portions in opensmtpd-action-local-delivery-configuration->string +This bit of code works in the repl too! +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +((@@ (opensmtpd-records) opensmtpd-action-local-delivery-configuration->string) + (opensmtpd-action-local-delivery-configuration + (userbase + (opensmtpd-table-configuration + (name "this") + (data (list "job" "done")))))) +#+END_SRC + +***** DONE virtual [2/2] +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 03:53] +:END: +****** DONE change the sanitize portion of the fieldname virtual in the + +The below code does work in a REPL. +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) +(opensmtpd-action-local-delivery-configuration + (virtual + (opensmtpd-table-configuration + (name "this") + (data (list "job" "done"))))) +#+END_SRC + +#+RESULTS: +****** DONE change relevant portions in opensmtpd-action-local-delivery-configuration->string +This bit of code works in the repl too! +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +((@@ (opensmtpd-records) opensmtpd-action-local-delivery-configuration->string) + (opensmtpd-action-local-delivery-configuration + (virtual + (opensmtpd-table-configuration + (name "this") + (data (list "job" "done")))))) + +#+END_SRC + +**** DONE opensmtpd-relay-configuration [4/4] +***** DONE helo-src +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:00] +:END: +***** DONE domain == [2/2] +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:00] +:END: + +****** DONE change the sanitize portion of the fieldname domain in the + +The below code does work in a REPL. +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +(opensmtpd-action-relay-configuration + (domain + (opensmtpd-table-configuration + (name "this") + (data (list "gnucode.me" "gnu-hurd.com"))))) + +#+END_SRC + +#+RESULTS: +****** DONE change relevant portions in opensmtpd-action-relay-configuration->string +This bit of code works in the repl too! +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +((@@ (opensmtpd-records) opensmtpd-action-relay-configuration->string) + (opensmtpd-action-relay-configuration + (domain + (opensmtpd-table-configuration + (name "this") + (data (list "gnucode.me" "gnu-hurd.com")))))) + + +#+END_SRC + +***** DONE auth == [2/2] +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:00] +:END: +****** DONE change the sanitize portion of the fieldname == in the + +The below code does work in a REPL. +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +(opensmtpd-action-relay-configuration + (auth + (opensmtpd-table-configuration + (name "this") + (data (list "gnucode.me" "gnu-hurd.com"))))) + +#+END_SRC + +#+RESULTS: +****** DONE change relevant portions in opensmtpd-action-relay-configuration->string +This bit of code works in the repl too! +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +((@@ (opensmtpd-records) opensmtpd-action-relay-configuration->string) + (opensmtpd-action-relay-configuration + (auth + (opensmtpd-table-configuration + (name "this") + (data (list "gnucode.me" "gnu-hurd.com")))))) + + +#+END_SRC + +***** DONE src srcaddress | +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:01] +:END: +****** DONE change the sanitize portion of the fieldname == in the + +The below code does work in a REPL. +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +(opensmtpd-action-relay-configuration + (src + (opensmtpd-table-configuration + (name "this") + (data (list "gnucode.me" "gnu-hurd.com"))))) + +#+END_SRC + +#+RESULTS: +****** DONE change relevant portions in opensmtpd-action-relay-configuration->string +This bit of code works in the repl too! +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +((@@ (opensmtpd-records) opensmtpd-action-relay-configuration->string) + (opensmtpd-action-relay-configuration + (src + (opensmtpd-table-configuration + (name "this") + (data (list "gnucode.me" "gnu-hurd.com")))))) + + +#+END_SRC + +Use the string scraddress or list table for the source IP address. +**** DONE opensmtpd-listen-on-configuration [3/3] +***** DONE auth == [3/3] +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:01] +:END: +****** DONE change the sanitize portion of the fieldname 'auth' in the + +The below code does work in a REPL. +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +(opensmtpd-listen-on-configuration + (auth + (opensmtpd-table-configuration + (name "My-table") + (data '(("joshua" . "$some$Long$EncrytpedPassword")))))) +#+END_SRC + +#+RESULTS: +****** DONE change relevant portions in opensmtpd-listen-on-configuration->string +This bit of code works in the repl too! +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +((@@ (opensmtpd-records) opensmtpd-listen-on-configuration->string) + (opensmtpd-listen-on-configuration + (auth + (opensmtpd-table-configuration + (name "credentials") + (data '(("joshua" . "$someLongEncrytpedPassword"))))))) +#+END_SRC + +****** DONE sanitize the == so that it can only be an opensmtpd-table-configuration, whose fieldname values are an assoc-list + +#+BEGIN_SRC scheme +(opensmtpd-listen-on-configuration (auth (opensmtpd-table-configuration (name "the") (data (list "the" "cat"))))) +#+END_SRC + +#+RESULTS: == fieldname: 'auth' is of type boolean, or an == record whose fieldname 'values' are an assoc-list. + +***** DONE auth-optional == [2/2] +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:01] +:END: +****** DONE change the sanitize portion of the fieldname 'auth-optional' in the + +The below code does work in a REPL. +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +(opensmtpd-listen-on-configuration + (auth + (opensmtpd-table-configuration + (name "My-table") + (data '(("joshua" . "$some$Long$EncrytpedPassword")))))) +#+END_SRC + +#+RESULTS: + +AND the below code will correctly result in an error! +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +(opensmtpd-listen-on-configuration + (auth + (opensmtpd-table-configuration + (name "My-table") + (data '("joshua" "$some$Long$EncrytpedPassword"))))) +#+END_SRC + +#+RESULTS: +: == fieldname: 'auth' is of type boolean, or an == record whose fieldname 'values' are an assoc-list +: (eg: (opensmtpd-table-configuration (name "table") (data '("joshua" . "$encrypted$password")))). + +****** DONE change relevant portions in opensmtpd-listen-on-configuration->string +This bit of code works in the repl too! +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +((@@ (opensmtpd-records) opensmtpd-listen-on-configuration->string) + (opensmtpd-listen-on-configuration + (auth + (opensmtpd-table-configuration + (name "credentials") + (data '(("joshua" . "$someLongEncrytpedPassword"))))))) +#+END_SRC + +***** DONE hostnames == [2/2] +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:01] +:END: +****** DONE change the sanitize portion of the fieldname 'hostnames' in the + +The below code does work in a REPL. +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +(opensmtpd-listen-on-configuration + (hostnames + (opensmtpd-table-configuration + (name "My-table") + (data '(("joshua" . "$some$Long$EncrytpedPassword")))))) +#+END_SRC + +#+RESULTS: + +AND the below code will correctly result in an error! +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +(opensmtpd-listen-on-configuration + (hostnames + (opensmtpd-table-configuration + (name "My-table") + (data '("joshua" "$some$Long$EncrytpedPassword"))))) +#+END_SRC + +#+RESULTS: +: == fieldname: 'hostname' is of type boolean, or an == record whose fieldname 'values' are an assoc-list +: (eg: (opensmtpd-table-configuration (name "table") (data '("joshua" . "$encrypted$password")))). + +****** DONE change relevant portions in opensmtpd-listen-on-configuration->string +This bit of code works in the repl too! +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +((@@ (opensmtpd-records) opensmtpd-listen-on-configuration->string) + (opensmtpd-listen-on-configuration + (hostnames + (opensmtpd-table-configuration + (name "credentials") + (data '(("joshua" . "$someLongEncrytpedPassword"))))))) +#+END_SRC + +**** TODO opensmtpd-match [20/24] +******* NO list approach +Guix probably won't like the list approach. +#+BEGIN_SRC scheme + (openmstpd-match + (for + (list 'not "for domain regex" + (opensmtpd-table-configuration + (data (list "gnucode.me" "gnu-hurd.com")))))) +#+END_SRC + +#+BEGIN_SRC scheme + (openmstpd-match + (for + (list "! for domain" + (opensmtpd-table-configuration + (data (list "gnucode.me" "gnu-hurd.com")))))) +#+END_SRC +******* opensmtpd-options-configuration approach I like this one quite a bit. + +This method is a little bit more verbose. Well I guess it's a lot +more verbose. But it's easier for me to properly parse what the user wants. + +I would sanitize the options in the opensmtpd-match-configuration-for, +openmsmtpd-match-from, opensmtpd-match-configuration-auth, opensmtpd-match-configuration-helo, +opensmtpd-match-configuration-mail-from, opensmtpd-match-configuration-rcpt-to fieldnames. +********* for +#+BEGIN_SRC scheme + (openmstpd-match + (for + (opensmtpd-options-configuration + (not #t) + (method "domain regex") ;; valid options for "for" are "domain" or "domain regex" + (opensmtpd-table-configuration + (data (list "gnucode.me" "gnu-hurd.com")))))) +#+END_SRC + +Do I want a regex fieldname? Probably not. It makes it more verbose... + +#+BEGIN_SRC scheme + (openmstpd-match + (for + (opensmtpd-options-configuration + (not #t) + (regex #t) + (method "domain") ;; valid options for "for" are "domain" + (opensmtpd-table-configuration + (data (list "gnucode.me" "gnu-hurd.com")))))) +#+END_SRC + +********* from +#+BEGIN_SRC scheme + (openmstpd-match + (from + (opensmtpd-options-configuration + (not #t) + (method "rdns regex") ;;valid options for from are "auth" "auth regex", "mail-from" "mail-from regex", + ;; "rdns", "rdns regex", "src", "src regex" + (opensmtpd-table-configuration + (data (list "gnucode.me" "gnu-hurd.com")))))) +#+END_SRC + +Do I want a regex fieldname? + +#+BEGIN_SRC scheme + (openmstpd-match + (from + (opensmtpd-options-configuration + (not #t) + (regex #t) + (method "rdns") ;;valid options for from are "auth", "mail-from", "rdns", "src" + (opensmtpd-table-configuration + (data (list "gnucode.me" "gnu-hurd.com")))))) +#+END_SRC + +********* auth +#+BEGIN_SRC scheme + (openmstpd-match + (auth + (opensmtpd-options-configuration + (not #t) + (method "auth regex") + (opensmtpd-table-configuration + (data (list "gnucode.me" "gnu-hurd.com")))))) +#+END_SRC + +Do I want a regex fieldname? + +#+BEGIN_SRC scheme + (openmstpd-match + (auth + (opensmtpd-options-configuration + (not #t) + (regex #t) + (method "auth") ;; valid options for auth are "auth" or this method can be left blank. + (opensmtpd-table-configuration + (data (list "gnucode.me" "gnu-hurd.com")))))) +#+END_SRC + +********* mail-from +#+BEGIN_SRC scheme + (openmstpd-match + (mail-from + (opensmtpd-options-configuration + (not #t) + (method "mail from") + (opensmtpd-table-configuration + (data (list "gnucode.me" "gnu-hurd.com")))))) +#+END_SRC +****** I tweak opensmtpd-match record and add a opensmtpd-options + +#+BEGIN_SRC scheme + (opensmtpd-match-configuration + (name "action-name") + (options + (list + (opensmtpd-options-configuration + (method "for domain regex"))) + )) +#+END_SRC + +****** PROJ many of these options are not completely sanitized. + +For example: "for domain" requires a domain | BUT this record, which +does not have a domain gives no errors: + +#+BEGIN_SRC scheme +(opensmtpd-match-configuration + (for (opensmtpd-option-configuration + (option "for domain"))) + (action (opensmtpd-action-local-delivery-configuration + (name "local") ))) +#+END_SRC + +And there are a ton of other examples of this. +****** DONE for domain +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:17] +:END: + +The datastructures work: + +#+BEGIN_SRC scheme +(opensmtpd-match-configuration + (name "local") + (for (opensmtpd-option-configuration + (option "for domain") + (value (opensmtpd-table-configuration + (name "this") + (data (list "helo" "hello")))))) + (action (opensmtpd-action-local-delivery-configuration))) +#+END_SRC + +#Results +: $4 = #< name: "local" action: #< method: "mbox" alias: #f ttl: #f user: #f userbase: #f virtual: #f wrapper: #f> for: #< option: "for domain" not: #f regex: #f value: #< name: "this" file-db: #f values: ("helo" "hello") type: #:148:97 (x)>>> from: #f auth: #f helo: #f mail-from: #f rcpt-to: #f tag: #f tls: #f> + +****** DONE for domain regexp +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:19] +:END: + +the datastructure works +#+BEGIN_SRC scheme +(opensmtpd-match-configuration + (name "local") + (for (opensmtpd-option-configuration + (regex #t) + (option "for domain") + (value (opensmtpd-table-configuration + (name "this") + (data (list "helo" "hello")))))) + (action (opensmtpd-action-local-delivery-configuration))) +#+END_SRC + +#Results +: $4 = #< name: "local" action: #< method: "mbox" alias: #f ttl: #f user: #f userbase: #f virtual: #f wrapper: #f> for: #< option: "for domain" not: #f regex: #f value: #< name: "this" file-db: #f values: ("helo" "hello") type: #:148:97 (x)>>> from: #f auth: #f helo: #f mail-from: #f rcpt-to: #f tag: #f tls: #f> + +****** DONE make opensmtpd-match-configuration->string work print for rcpt the appropriate match lines if some values now accept + +Seems to work: + +#+BEGIN_SRC scheme +(opensmtpd-match-configuration->string (opensmtpd-match-configuration + (name "local") + (for (opensmtpd-option-configuration + (regex #t) + (option "for domain") + (value (opensmtpd-table-configuration + (name "this") + (data (list "helo" "hello")))))) + (action (opensmtpd-action-local-delivery-configuration)))) +$6 = "match for domain regex == action \"local\" \n" +#+END_SRC + +also seems to work + +#+BEGIN_SRC scheme +(opensmtpd-match-configuration->string (opensmtpd-match-configuration + (name "local") + (for (opensmtpd-option-configuration + (option "for domain") + (value (opensmtpd-table-configuration + (name "this") + (data (list "helo" "hello")))))) + (action (opensmtpd-action-local-delivery-configuration)))) +$7 = "match for domain == action \"local\" \n" +#+END_SRC +****** DONE for rcpt +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:19] +:END: +****** DONE for rcpt regexp +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:19] +:END: +****** DONE from auth user | +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:19] +:END: + +#+BEGIN_SRC scheme +(opensmtpd-option-configuration + (regex #t) + (option "from auth") + (value (opensmtpd-table-configuration + (name "this") + (data (list "helo" "hello"))))) +$8 = #< option: "from auth" not: #f regex: #t value: #< name: "this" file-db: #f values: ("helo" "hello") type: #:224:14 (x)>>> + +#+END_SRC +****** DONE from auth regex user | +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:20] +:END: +****** DONE make sure opensmtpd-option-configuration->string works for from auth if they use + +#+BEGIN_SRC scheme +(opensmtpd-option-configuration->string + (opensmtpd-option-configuration + (regex #t) + (option "from auth") + (value (opensmtpd-table-configuration + (name "this") + (data (list "helo" "hello")))))) +$10 = "from auth regex == " +#+END_SRC +****** DONE from mail-from sender | +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:20] +:END: +****** DONE from mail-from regexp +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:20] +:END: +****** DONE from rdns +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:20] +:END: +****** DONE from rdns regex +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:20] +:END: +****** DONE from src
+:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:21] +:END: +****** DONE from src regex
+:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:21] +:END: +****** TODO do some more sanitizing of these last couple of options +There may be some way to specify invalid data. For example: + +tls does not support regex, not, or value fields. The below code should be an error. +#+BEGIN_SRC scheme + (display (opensmtpd-match-configuration->string (opensmtpd-match-configuration + (tls (opensmtpd-option-configuration + (option "tls") ;; this should be auth!!! NOT "helo" + (regex #t) + (not #t) + (value (opensmtpd-table-configuration (name "mytable") + (data (list "cat" "kitten")))))) + (from (opensmtpd-option-configuration (option "from rdns") + (value (opensmtpd-table-configuration (name "table") + (data (list "cat" "hat")))))) + (action (opensmtpd-action-local-delivery-configuration + (name "matches")))))) +match from rdns
! tls regex action "matches" +#+END_SRC + +****** DONE auth +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:21] +:END: + +Well I need to fix this bug. Basically, I can make an + with a table of values for 'option' "auth". And +I can print that table with (opensmtpd-option-configuration->string)...That works +fine... + +But if I put that same record into an == ...for some reason that +'auth' table is not being printed. + +#+BEGIN_SRC scheme +(opensmtpd-option-configuration + (option "auth") + (value (opensmtpd-table-configuration (name "mytable") + (data (list "cat" "hat"))))) +$20 = #< option: "auth" not: #f regex: #f value: #< name: "mytable" file-db: #f values: ("cat" "hat") type: #:860:40 (x)>>> +scheme@(opensmtpd-records) [4]> (opensmtpd-option-configuration->string $20) +$21 = "auth == " +scheme@(opensmtpd-records) [4]> (opensmtpd-match (name "matches") + (auth (opensmtpd-option-configuration + (option "auth") + (value (opensmtpd-table-configuration (name "mytable") + (data (list "cat" "kitten")))))) + (from (opensmtpd-option-configuration (option "from rdns") + (value (opensmtpd-table-configuration (name "table") + (data (list "cat" "hat")))))) + (action (opensmtpd-action-local-delivery-configuration))) +$22 = #< name: "matches" action: #< method: "mbox" alias: #f ttl: #f user: #f userbase: #f virtual: #f wrapper: #f> for: #f from: #< option: "from rdns" not: #f regex: #f value: #< name: "table" file-db: #f values: ("cat" "hat") type: #:876:89 (x)>>> auth: #< option: "auth" not: #f regex: #f value: #< name: "mytable" file-db: #f values: ("cat" "kitten") type: #:873:63 (x)>>> helo: #f mail-from: #f rcpt-to: #f tag: #f tls: #f> +scheme@(opensmtpd-records) [5]> (opensmtpd-match-configuration->string $22) +$23 = "match from rdns =
= auth action \"matches\" \n" ;; THERE IS SUPPOSED TO BE a "auth " here +scheme@(opensmtpd-records) [5]> +#+END_SRC +****** TODO [!] auth +****** TODO auth regex +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:21] +:END: + +This does NOT show the regex for the auth option. or the table why? +#+BEGIN_SRC scheme +(opensmtpd-match-configuration + (auth (opensmtpd-option-configuration + (option "auth") ;; this should be auth!!! NOT "helo" + (regex #t) + (value (opensmtpd-table-configuration (name "mytable") + (data (list "cat" "kitten")))))) + (from (opensmtpd-option-configuration (option "from rdns") + (value (opensmtpd-table-configuration (name "table") + (data (list "cat" "hat")))))) + (action (opensmtpd-action-local-delivery-configuration + (name "matches")))) +$9 = #< action: #< name: "matches" method: "mbox" alias: #f ttl: #f user: #f userbase: #f virtual: #f wrapper: #f> for: #f from: #< option: "from rdns" not: #f regex: #f value: #< name: "table" file-db: #f values: ("cat" "hat") type: #:144:52 (x)>>> auth: #< option: "auth" not: #f regex: #t value: #< name: "mytable" file-db: #f values: ("cat" "kitten") type: #:141:15 (x)>>> helo: #f mail-from: #f rcpt-to: #f tag: #f tls: #f> +scheme@(opensmtpd-records) [4]> (display (opensmtpd-match-configuration->string $9)) +match from rdns
auth action "matches" ;; there should be a regex in there. +#+END_SRC +****** DONE helo +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:21] +:END: +#+BEGIN_SRC scheme +(opensmtpd-match-configuration + (helo (opensmtpd-option-configuration + (option "helo") ;; this should be auth!!! NOT "helo" + (regex #t) + (value (opensmtpd-table-configuration (name "mytable") + (data (list "cat" "kitten")))))) + (from (opensmtpd-option-configuration (option "from rdns") + (value (opensmtpd-table-configuration (name "table") + (data (list "cat" "hat")))))) + (action (opensmtpd-action-local-delivery-configuration + (name "matches")))) +$10 = #< action: #< name: "matches" method: "mbox" alias: #f ttl: #f user: #f userbase: #f virtual: #f wrapper: #f> for: #f from: #< option: "from rdns" not: #f regex: #f value: #< name: "table" file-db: #f values: ("cat" "hat") type: #:252:52 (x)>>> auth: #f helo: #< option: "helo" not: #f regex: #t value: #< name: "mytable" file-db: #f values: ("cat" "kitten") type: #:249:15 (x)>>> mail-from: #f rcpt-to: #f tag: #f tls: #f> +scheme@(opensmtpd-records) [5]> (opensmtpd-match-configuration->string $10) +$11 = "match from rdns
helo regex action \"matches\" \n" +#+END_SRC +****** DONE mail-from +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:22] +:END: +#+BEGIN_SRC scheme + (display (opensmtpd-match-configuration->string (opensmtpd-match-configuration + (mail-from (opensmtpd-option-configuration + (option "mail-from") ;; this should be auth!!! NOT "helo" + (regex #t) + (not #t) + (value (opensmtpd-table-configuration (name "mytable") + (data (list "cat" "kitten")))))) + (from (opensmtpd-option-configuration (option "from rdns") + (value (opensmtpd-table-configuration (name "table") + (data (list "cat" "hat")))))) + (action (opensmtpd-action-local-delivery-configuration + (name "matches")))))) +match from rdns
! mail-from regex action "matches" +#+END_SRC +****** DONE mail-from regex +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:22] +:END: +#+BEGIN_SRC scheme + (display (opensmtpd-match-configuration->string (opensmtpd-match-configuration + (mail-from (opensmtpd-option-configuration + (option "mail-from") ;; this should be auth!!! NOT "helo" + (regex #t) + (not #t) + (value (opensmtpd-table-configuration (name "mytable") + (data (list "cat" "kitten")))))) + (from (opensmtpd-option-configuration (option "from rdns") + (value (opensmtpd-table-configuration (name "table") + (data (list "cat" "hat")))))) + (action (opensmtpd-action-local-delivery-configuration + (name "matches")))))) +match from rdns
! mail-from regex action "matches" +#+END_SRC +****** DONE rcpt-to +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:22] +:END: +****** DONE rcpt-to regex +:LOGBOOK: +- State "TODO" from "TODO" [2021-11-02 Tue 04:23] +:END: + +*** PROJ sanitize the == records in == & == +**** PROJ testing the sanitize-list-of-options-for-match-configuration-assoc precodure [5/5] +***** DONE make sure each option is unique (no duplicate "for"s). + +#+BEGIN_SRC scheme +(opensmtpd-configuration + (matches (list + (opensmtpd-match-configuration + (options (list + (opensmtpd-option-configuration + (option "for any")) + (opensmtpd-option-configuration + (option "for local")))) + (action + (opensmtpd-action-relay-configuration + (name "relay"))))))) +#+END_SRC +***** DONE make sure there is no duplicate from's +#+BEGIN_SRC scheme +(opensmtpd-configuration + (matches (list + (opensmtpd-match-configuration + (options (list + (opensmtpd-option-configuration + (option "from any")) + (opensmtpd-option-configuration + (option "from auth")))) + (action + (opensmtpd-action-relay-configuration + (name "relay"))))))) +#+END_SRC +***** DONE for any data and regex must be false + +#+BEGIN_SRC scheme +(opensmtpd-configuration + (matches (list + (opensmtpd-match-configuration + (options (list + (opensmtpd-option-configuration + (option "for any") + (regex #t)))) + (action + (opensmtpd-action-relay-configuration + (name "relay"))))))) +#+END_SRC +***** DONE 'rcpt-to' must have data + +#+BEGIN_SRC scheme +(opensmtpd-configuration + (matches (list + (opensmtpd-match-configuration + (options (list + (opensmtpd-option-configuration + (option "rcpt-to")))) + (action + (opensmtpd-action-relay-configuration + (name "relay"))))))) +#+END_SRC +***** DONE 'tls' cannot have a 'data' or 'regex' + +#+BEGIN_SRC scheme +(opensmtpd-configuration + (matches (list + (opensmtpd-match-configuration + (options (list + (opensmtpd-option-configuration + (option "tls") + (data "hello") + ))) + (action + (opensmtpd-action-relay-configuration + (name "relay"))))))) +#+END_SRC +*** TODO sanitize fieldname 'matches' so that no two unique actions have the same name +I definitely should sanitize 'matches' a bit more. For example, you could have two different +actions, one for local delivery and one for remote, with the same name. I +should make sure that all unique actions have unique names. + +Here is an example of two actions that have the same name, but different ttl values: + +#+BEGIN_SRC scheme +(opensmtpd-configuration + (matches + (list (opensmtpd-match-configuration + (action + (opensmtpd-action-local-delivery-configuration + (name "my-local-delivery") + (ttl "50m")))) ; 50 minutes + (opensmtpd-match-configuration + (action + (opensmtpd-action-local-delivery-configuration + (name "my-local-delivery") + (ttl "50h"))))))) ; 50 hours +#+END_SRC + +*** PROJ follow the style guide and style up my project [0/4] +https://mumble.net/~campbell/scheme/style.txt + +:SchemeStyleGuide: +#+BEGIN_SRC org +Riastradh's Lisp Style Rules -*- outline -*- + + Copyright (C) 2007--2011 Taylor R. Campbell + + CC BY-NC-SA 3.0 + + This work is licensed under a Creative Commons + Attribution-NonCommercial-ShareAlike 3.0 Unported License: + . + +This is a guide to Lisp style, written by Taylor R. Campbell, to +describe the standard rules of Lisp style as well as a set of more +stringent rules for his own style. This guide should be useful for +Lisp in general, but there are [or will be in the final draft] parts +that are focussed on or specific to Scheme or Common Lisp. + +This guide is written primarily as a collection of rules, with +rationale for each rule. (If a rule is missing rationale, please +inform the author!) Although a casual reader might go through and read +the rules without the rationale, perhaps reasoning that reading of the +rationale would require more time than is available, such a reader +would derive little value from this guide. In order to apply the rules +meaningfully, their spirit must be understood; the letter of the rules +serves only to hint at the spirit. The rationale is just as important +as the rules. + +There are many references in this document to `Emacs', `GNU Emacs', +`Edwin', and so on. In this document, `Emacs' means any of a class of +editors related in design to a common ancestor, the EMACS editor macros +written for TECO on ITS on the PDP-10 in the middle of the nineteen +seventies. All such editors -- or `all Emacsen', since `Emacsen' is +the plural of `Emacs' -- have many traits in common, such as a very +consistent set of key bindings, extensibility in Lisp, and so on. `GNU +Emacs' means the member of the class of editors collectively known as +Emacsen that was written for the GNU Project in the middle of the +nineteen eighties, and which is today probably the most popular Emacs. +`Edwin' is MIT Scheme's Emacs, which is bundled as part of MIT Scheme, +and not available separately. There are other Emacsen as well, such as +Hemlock and Climacs, but as the author of this document has little +experience with Emacsen other than GNU Emacs and Edwin, there is little +mention of other Emacsen. + +This guide is a work in progress. To be written: + +- Indentation rules for various special operators. +- Philosophical rambling concerning naming. +- Rules for breaking lines. +- Many more examples. +- A more cohesive explanation of the author's principles for composing + programs, and their implications. +- Rules for writing portable code. +- Some thoughts concerning extensions to the lexical syntax. +- Rules for writing or avoiding macros. +- Some unfinished rationale. +- More on documentation. +- The `Dependencies' subsection of the `General Layout' section should + be put in a different section, the rest of which has yet to be + written, on organization of programs, module systems, and portable + code. + +Feedback is welcome; address any feedback by email to the host +mumble.net's user `campbell', or by IRC to Riastradh in the #scheme +channel on Freenode (irc.freenode.net). Feedback includes reports of +typos, questions, requests for clarification, and responses to the +rationale, except in the case of round brackets versus square +brackets, the argument surrounding which is supremely uninteresting +and now not merely a dead horse but a rotting carcass buzzing with +flies and being picked apart by vultures. + +As this document has grown, the line between standard Lisp rules and +the author's own style has been blurred. The author is considering +merging of the partition, but has not yet decided on this with +certainty. Opinions on the subject are welcome -- is the partition +still useful to keep the author's biases and idiosyncrasies out of the +standard rules, or has the partition with its arbitrary nature only +caused disorganization of the whole document? + +Unfortunately, this document is entirely unscientific. It is at best a +superstition or philosophy, but one that the author of this document +has found to have improved his programs. Furthermore, the author is +somewhat skeptical of claims of scientific analyses of these matters: +analyzing human behaviour, especially confined to the set of excellent +programmers who often have strong opinions about their methods for +building programs, is a very tricky task. + +,* Standard Rules + +These are the standard rules for formatting Lisp code; they are +repeated here for completeness, although they are surely described +elsewhere. These are the rules implemented in Emacs Lisp modes, and +auxiliary utilities such as Paredit. + +The rationale given here is merely the author's own speculation of the +origin of these rules, and should be taken as nothing more than it. +The reader shall, irrespective of the author's rationale, accept the +rules as sent by the reader's favourite deity, or Cthulhu if no such +deity strikes adequate fear into the heart of the reader. + +,** Parentheses + +,*** Terminology + +This guide avoids the term /parenthesis/ except in the general use of +/parentheses/ or /parenthesized/, because the word's generally accepted +definition, outside of the programming language, is a statement whose +meaning is peripheral to the sentence in which it occurs, and *not* the +typographical symbols used to delimit such statements. + +The balanced pair of typographical symbols that mark parentheses in +English text are /round brackets/, i.e. ( and ). There are several +other balanced pairs of typographical symbols, such as /square +brackets/ (commonly called simply `brackets' in programming circles), +i.e. [ and ]; /curly braces/ (sometimes called simply `braces'), i.e. { +and }; /angle brackets/ (sometimes `brokets' (for `broken brackets')), +i.e. < and >. + +In any balanced pair of typographical symbols, the symbol that begins +the region delimited by the symbols is called the /opening bracket/ or +the /left bracket/, such as ( or [ or { or <. The symbol that ends +that region is called the /right bracket/ or the /closing bracket/, +such as > or } or ] or ). + +,*** Spacing + +If any text precedes an opening bracket or follows a closing bracket, +separate that text from that bracket with a space. Conversely, leave +no space after an opening bracket and before following text, or after +preceding text and before a closing bracket. + + Unacceptable: + + (foo(bar baz)quux) + (foo ( bar baz ) quux) + + Acceptable: + + (foo (bar baz) quux) + + Rationale: This is the same spacing found in standard typography of + European text. It is more aesthetically pleasing. + +,*** Line Separation + +Absolutely do *not* place closing brackets on their own lines. + + Unacceptable: + + (define (factorial x) + (if (< x 2) + 1 + (* x (factorial (- x 1 + ) + ) + ) + ) + ) + + Acceptable: + + (define (factorial x) + (if (< x 2) + 1 + (* x (factorial (- x 1))))) + + Rationale: The parentheses grow lonely if their closing brackets are + all kept separated and segregated. + +,**** Exceptions to the Above Rule Concerning Line Separation + +Do not heed this section unless you know what you are doing. Its title +does *not* make the unacceptable example above acceptable. + +When commenting out fragments of expressions with line comments, it may +be necessary to break a line before a sequence of closing brackets: + + (define (foo bar) + (list (frob bar) + (zork bar) + ;; (zap bar) + )) + +This is acceptable, but there are other alternatives. In Common Lisp, +one can use the read-time optional syntax, `#+' or `#-', with a +feature optional that is guaranteed to be false or true -- `#+(OR)' +or `#-(AND)' --; for example, + + (define (foo bar) + (list (frob bar) + (zork bar) + ,#+(or) (zap bar))). + +Read-time optionals are expression-oriented, not line-oriented, so +the closing brackets need not be placed on the following line. Some +Scheme implementations, and SRFI 62, also support expression comments +with `#;', which are operationally equivalent to the above read-time +optionals for Common Lisp: + + (define (foo bar) + (list (frob bar) + (zork bar) + #; + (zap bar))) + +The expression is placed on another line in order to avoid confusing +editors that do not recognize S-expression comments; see the section +titled `Comments' below for more details. However, the `#;' notation +is not standard -- it appears in neither the IEEE 1178 document nor in +the R5RS --, so line comments are preferable for portable Scheme code, +even if they require breaking a line before a sequence of closing +brackets. + +Finally, it is acceptable to break a line immediately after an opening +bracket and immediately before a closing bracket for very long lists, +especially in files under version control. This eases the maintenance +of the lists and clarifies version diffs. Example: + + (define colour-names ;Add more colour names to this list! + '( + blue + cerulean + green + magenta + purple + red + scarlet + turquoise + )) + +,*** Parenthetical Philosophy + +The actual bracket characters are simply lexical tokens to which little +significance should be assigned. Lisp programmers do not examine the +brackets individually, or, Azathoth forbid, count brackets; instead +they view the higher-level structures expressed in the program, +especially as presented by the indentation. Lisp is not about writing +a sequence of serial instructions; it is about building complex +structures by summing parts. The composition of complex structures +from parts is the focus of Lisp programs, and it should be readily +apparent from the Lisp code. Placing brackets haphazardly about the +presentation is jarring to a Lisp programmer, who otherwise would not +even have seen them for the most part. + +,** Indentation and Alignment + +The operator of any form, i.e. the first subform following the opening +round bracket, determines the rules for indenting or aligning the +remaining forms. Many names in this position indicate special +alignment or indentation rules; these are special operators, macros, or +procedures that have certain parameter structures. + +If the first subform is a non-special name, however, then if the second +subform is on the same line, align the starting column of all following +subforms with that of the second subform. If the second subform is on +the following line, align its starting column with that of the first +subform, and do the same for all remaining subforms. + +In general, Emacs will indent Lisp code correctly. Run `C-M-q' +(indent-sexp) on any code to ensure that it is indented correctly, and +configure Emacs so that any non-standard forms are indented +appropriately. + + Unacceptable: + + (+ (sqrt -1) + (* x y) + (+ p q)) + + (+ + (sqrt -1) + (* x y) + (+ p q)) + + Acceptable: + + (+ (sqrt -1) + (* x y) + (+ p q)) + + (+ + (sqrt -1) + (* x y) + (+ p q)) + + Rationale: The columnar alignment allows the reader to follow the + operands of any operation straightforwardly, simply by scanning + downward or upward to match a common column. Indentation dictates + structure; confusing indentation is a burden on the reader who wishes + to derive structure without matching parentheses manually. + +,*** Non-Symbol Indentation and Alignment + +The above rules are not exhaustive; some cases may arise with strange +data in operator positions. + +,**** Lists + +Unfortunately, style varies here from person to person and from editor +to editor. Here are some examples of possible ways to indent lists +whose operators are lists: + + Questionable: + + ((car x) ;Requires hand indentation. + (cdr x) + foo) + + ((car x) (cdr x) ;GNU Emacs + foo) + + Preferable: + + ((car x) ;Any Emacs + (cdr x) + foo) + + ((car x) (cdr x) ;Edwin + foo) + + Rationale: The operands should be aligned, as if it were any other + procedure call with a name in the operator position; anything other + than this is confusing because it gives some operands greater visual + distinction, allowing others to hide from the viewer's sight. For + example, the questionable indentation + + ((car x) (cdr x) + foo) + + can make it hard to see that FOO and (CDR X) are both operands here + at the same level. However, GNU Emacs will generate that indentation + by default. (Edwin will not.) + +,**** Strings + +If the form in question is meant to be simply a list of literal data, +all of the subforms should be aligned to the same column, irrespective +of the first subform. + + Unacceptable: + + ("foo" "bar" "baz" "quux" "zot" + "mumble" "frotz" "gargle" "mumph") + + Questionable, but acceptable: + + (3 1 4 1 5 9 2 6 5 3 5 8 9 7 9 3 2 3 8 4 6 2 6 4 + 3 3 8 3 2 7 9 5 0 2 8 8 4 1 9 7 1 6 9 3 9 9 3) + + Acceptable: + + ("foo" "bar" "baz" "quux" "zot" + "mumble" "frotz" "gargle" "mumph") + + ("foo" + "bar" "baz" "quux" "zot" + "mumble" "frotz" "gargle" "mumph") + + Rationale: Seldom is the first subform distinguished for any reason, + if it is a literal; usually in this case it indicates pure data, not + code. Some editors and pretty-printers, however, will indent + unacceptably in the example given unless the second subform is on the + next line anyway, which is why the last way to write the fragment is + usually best. + +,** Names + +Naming is subtle and elusive. Bizarrely, it is simultaneously +insignificant, because an object is independent of and unaffected by +the many names by which we refer to it, and also of supreme +importance, because it is what programming -- and, indeed, almost +everything that we humans deal with -- is all about. A full +discussion of the concept of name lies far outside the scope of this +document, and could surely fill not even a book but a library. + +Symbolic names are written with English words separated by hyphens. +Scheme and Common Lisp both fold the case of names in programs; +consequently, camel case is frowned upon, and not merely because it is +ugly. Underscores are unacceptable separators except for names that +were derived directly from a foreign language without translation. + + Unacceptable: + + XMLHttpRequest + foreach + append_map + + Acceptable: + + xml-http-request + for-each + append-map + +,*** Funny Characters + +There are several different conventions in different Lisps for the use +of non-alphanumeric characters in names. + +,**** Scheme + +,***** Question Marks: Predicates + +Affix a question mark to the end of a name for a procedure whose +purpose is to ask a question of an object and to yield a boolean +answer. Such procedures are called `predicates'. Do not use a +question mark if the procedure may return any object other than a +boolean. + + Examples: pair? procedure? proper-list? + Non-examples: member assoc any every + +Pronounce the question mark as if it were the isolated letter `p'. For +example, to read the fragment (PAIR? OBJECT) aloud, say: `pair-pee +object.' + +,***** Exclamation Marks: Destructive Operations + +Affix an exclamation mark to the end of a name for a procedure (or +macro) whose primary purpose is to modify an object. Such procedures +are called `destructive'. + + Examples: set-car! append! + +Avoid using the exclamation mark willy nilly for just *any* procedure +whose operation involves any kind of mutation or side effect; instead, +use the exclamation mark to identify procedures that exist *solely* for +the purpose of destructive update (e.g., SET-CAR!), or to distinguish a +destructive, or potentially destructive (in the case of linear-update +operations such as APPEND!), variant of a procedure of which there also +exists a purely functional variant (e.g., APPEND). + +Pronounce the exclamation mark as `bang'. For example, to read the +fragment (APPEND! LIST TAIL) aloud, say: `append-bang list tail.' + +,***** Asterisks: Variants, Internal Routines, Mutable Globals + +Affix an asterisk to the end of a name to make a variation on a theme +of the original name. + + Example: let -> let* + +Prefer a meaningful name over an asterisk; the asterisk does not +explain what variation on the theme the name means. + +Affix an asterisk to the beginning of a name to make an internal +routine for that name. Again, prefer a meaningful name over an +asterisk. + +Affix asterisks to the beginning and end of a globally mutable +variable. This allows the reader of the program to recognize very +easily that it is badly written! + +,***** `WITH-' and `CALL-WITH-': Dynamic State and Cleanup + +Prefix `WITH-' to any procedure that establishes dynamic state and +calls a nullary procedure, which should be the last (required) +argument. The dynamic state should be established for the extent of +the nullary procedure, and should be returned to its original state +after that procedure returns. + + Examples: with-input-from-file with-output-to-file + + Exception: Some systems provide a procedure (WITH-CONTINUATION + ), which calls in the given + continuation, using that continuation's dynamic state. If + returns, it will return to , not to the continuation of + the call to WITH-CONTINUATION. This is acceptable. + +Prefix `CALL-WITH-' to any procedure that calls a procedure, which +should be its last argument, with some arguments, and is either somehow +dependent upon the dynamic state or continuation of the program, or +will perform some action to clean up data after the procedure argument +returns. Generally, `CALL-WITH-' procedures should return the values +that the procedure argument returns, after performing the cleaning +action. + + Examples: + + - CALL-WITH-INPUT-FILE and CALL-WITH-OUTPUT-FILE both accept a + pathname and a procedure as an argument, open that pathname (for + input or output, respectively), and call the procedure with one + argument, a port corresponding with the file named by the given + pathname. After the procedure returns, CALL-WITH-INPUT-FILE and + CALL-WITH-OUTPUT-FILE close the file that they opened, and return + whatever the procedure returned. + + - CALL-WITH-CURRENT-CONTINUATION is dependent on the continuation + with which it was called, and passes as an argument an escape + procedure corresponding with that continuation. + + - CALL-WITH-OUTPUT-STRING, a common but non-standard procedure + definable in terms of OPEN-OUTPUT-STRING and GET-OUTPUT-STRING from + SRFI 6 (Basic String Ports), calls its procedure argument with an + output port, and returns a string of all of the output written to + that port. Note that it does not return what the procedure + argument returns, which is an exception to the above rule. + +Generally, the distinction between these two classes of procedures is +that `CALL-WITH-...' procedures should not establish fresh dynamic +state and instead pass explicit arguments to their procedure arguments, +whereas `WITH-...' should do the opposite and establish dynamic state +while passing zero arguments to their procedure arguments. + +,** Comments + +Write heading comments with at least four semicolons; see also the +section below titled `Outline Headings'. + +Write top-level comments with three semicolons. + +Write comments on a particular fragment of code before that fragment +and aligned with it, using two semicolons. + +Write margin comments with one semicolon. + +The only comments in which omission of a space between the semicolon +and the text is acceptable are margin comments. + + Examples: + + ;;;; Frob Grovel + + ;;; This section of code has some important implications: + ;;; 1. Foo. + ;;; 2. Bar. + ;;; 3. Baz. + + (define (fnord zarquon) + ;; If zob, then veeblefitz. + (quux zot + mumble ;Zibblefrotz. + frotz)) + +,* Riastradh's Non-Standard Rules + +Three principles guide this style, roughly ordered according to +descending importance: + +1. The purpose of a program is to describe an idea, and not the way + that the idea must be realized; the intent of the program's meaning, + rather than peripheral details that are irrelevant to its intent, + should be the focus of the program, *irrespective* of whether a + human or a machine is reading it. [It would be nice to express this + principle more concisely.] + +2. The sum of the parts is easier to understand than the whole. + +3. Aesthetics matters. No one enjoys reading an ugly program. + +,** General Layout + +This section contains rules that the author has found generally helpful +in keeping his programs clean and presentable, though they are not +especially philosophically interesting. + +Contained in the rationale for some of the following rules are +references to historical limitations of terminals and printers, which +are now considered aging cruft of no further relevance to today's +computers. Such references are made only to explain specific measures +chosen for some of the rules, such as a limit of eighty columns per +line, or sixty-six lines per page. There is a real reason for each of +the rules, and this real reason is not intrinsically related to the +historical measures, which are mentioned only for the sake of +providing some arbitrary measure for the limit. + +,*** File Length + +If a file exceeds five hundred twelve lines, begin to consider +splitting it into multiple files. Do not write program files that +exceed one thousand twenty-four lines. Write a concise but +descriptive title at the top of each file, and include no content in +the file that is unrelated to its title. + + Rationale: Files that are any larger should generally be factored + into smaller parts. (One thousand twenty-four is a nicer number than + one thousand.) Identifying the purpose of the file helps to break it + into parts if necessary and to ensure that nothing unrelated is + included accidentally. + +,*** Top-Level Form Length + +Do not write top-level forms that exceed twenty-one lines, except for +top-level forms that serve only the purpose of listing large sets of +data. If a procedure exceeds this length, split it apart and give +names to its parts. Avoid names formed simply by appending a number +to the original procedure's name; give meaningful names to the parts. + + Rationale: Top-level forms, especially procedure definitions, that + exceed this length usually combine too many concepts under one name. + Readers of the code are likely to more easily understand the code if + it is composed of separately named parts. Simply appending a number + to the original procedure's name can help only the letter of the + rule, not the spirit, however, even if the procedure was taken from a + standard algorithm description. Using comments to mark the code with + its corresponding place in the algorithm's description is acceptable, + but the algorithm should be split up in meaningful fragments anyway. + + Rationale for the number twenty-one: Twenty-one lines, at a maximum + of eighty columns per line, fits in a GNU Emacs instance running in a + 24x80 terminal. Although the terminal may have twenty-four lines, + three of the lines are occupied by GNU Emacs: one for the menu bar + (which the author of this guide never uses, but which occupies a line + nevertheless in a vanilla GNU Emacs installation), one for the mode + line, and one for the minibuffer's window. The writer of some code + may not be limited to such a terminal, but the author of this style + guide often finds it helpful to have at least four such terminals or + Emacs windows open simultaneously, spread across a twelve-inch laptop + screen, to view multiple code fragments. + +,*** Line Length + +Do not write lines that exceed eighty columns, or if possible +seventy-two. + + Rationale: Following multiple lines that span more columns is + difficult for humans, who must remember the line of focus and scan + right to left from the end of the previous line to the beginning of + the next line; the more columns there are, the harder this is to do. + Sticking to a fixed limit helps to improve readability. + + Rationale for the numbers eighty and seventy-two: It is true that we + have very wide screens these days, and we are no longer limited to + eighty-column terminals; however, we ought to exploit our wide + screens not by writing long lines, but by viewing multiple fragments + of code in parallel, something that the author of this guide does + very often. Seventy-two columns leave room for several nested layers + of quotation in email messages before the code reaches eighty + columns. Also, a fixed column limit yields nicer printed output, + especially in conjunction with pagination; see the section + `Pagination' below. + +,*** Blank Lines + +Separate each adjacent top-level form with a single blank line (i.e. +two line breaks). If two blank lines seem more appropriate, break the +page instead. Do not place blank lines in the middle of a procedure +body, except to separate internal definitions; if there is a blank +line for any other reason, split the top-level form up into multiple +ones. + + Rationale: More than one blank line is distracting and sloppy. If + the two concepts that are separated by multiple blank lines are + really so distinct that such a wide separator is warranted, then + they are probably better placed on separate pages anyway; see the + next section, `Pagination'. + +,*** Pagination + +Separate each file into pages of no more than sixty-six lines and no +fewer than forty lines with form feeds (ASCII #x0C, or ^L, written in +Emacs with `C-q C-l'), on either side of which is a single line break +(but not a blank line). + + Rationale: Keeping distinct concepts laid out on separate pages + helps to keep them straight. This is helpful not only for the + writer of the code, but also for the reader. It also allows readers + of the code to print it onto paper without fiddling with printer + settings to permit pages of more than sixty-six lines (which is the + default number for many printers), and pagination also makes the + code easier to navigate in Emacs, with the `C-x [' and `C-x ]' keys + (`backward-page' and `forward-page', respectively). To avoid + excessively small increments of page-by-page navigation, and to + avoid wasting paper, each page should generally exceed forty lines. + + `C-x l' in Emacs will report the number of lines in the page on which + the point lies; this is useful for finding where pagination is + necessary. + +,*** Outline Headings + +Use Emacs's Outline Mode to give titles to the pages, and if +appropriate a hierarchical structure. Set `outline-regexp' (or +`outline-pattern' in Edwin) to "\f\n;;;;+ ", so that each form feed +followed by an line break followed by at least four semicolons and a +space indicates an outline heading to Emacs. Use four semicolons for +the highest level of headings in the hierarchy, and one more for each +successively nested level of hierarchy. + + Rationale: Not only does this clarify the organization of the code, + but readers of the code can then navigate the code's structure with + Outline Mode commands such as `C-c C-f', `C-c C-b', `C-c C-u', and + `C-c C-d' (forward, backward, up, down, respectively, headings). + +,*** Dependencies + +When writing a file or module, minimize its dependencies. If there are +too many dependencies, consider breaking the module up into several +parts, and writing another module that is the sum of the parts and that +depends only on the parts, not their dependencies. + + Rationale: A fragment of a program with fewer dependencies is less + of a burden on the reader's cognition. The reader can more easily + understand the fragment in isolation; humans are very good at local + analyses, and terrible at global ones. + +,** Naming + +This section requires an elaborate philosophical discussion which the +author is too ill to have the energy to write at this moment. + +Compose concise but meaningful names. Do not cheat by abbreviating +words or using contractions. + + Rationale: Abbreviating words in names does not make them shorter; + it only makes them occupy less screen space. The reader still must + understand the whole long name. This does not mean, however, that + names should necessarily be long; they should be descriptive. Some + long names are more descriptive than some short names, but there are + also descriptive names that are not long and long names that are not + descriptive. Here is an example of a long name that is not + descriptive, from SchMUSE, a multi-user simulation environment + written in MIT Scheme: + + frisk-descriptor-recursive-subexpr-descender-for-frisk-descr-env + + Not only is it long (sixty-four characters) and completely + impenetrable, but halfway through its author decided to abbreviate + some words as well! + +Do not write single-letter variable names. Give local variables +meaningful names composed from complete English words. + + Rationale: It is tempting to reason that local variables are + invisible to other code, so it is OK to be messy with their names. + This is faulty reasoning: although the next person to come along and + use a library may not care about anything but the top-level + definitions that it exports, this is not the only audience of the + code. Someone will also want to read the code later on, and if it is + full of impenetrably terse variable names without meaning, that + someone will have a hard time reading the code. + +Give names to intermediate values where their expressions do not +adequately describe them. + + Rationale: An `expression' is a term that expresses some value. + Although a machine needs no higher meaning for this value, and + although it should be written to be sufficiently clear for a human to + understand what it means, the expression might mean something more + than just what it says where it is used. Consequently, it is helpful + for humans to see names given to expressions. + + Example: A hash table HASH-TABLE maps foos to bars; (HASH-TABLE/GET + HASH-TABLE FOO #F) expresses the datum that HASH-TABLE maps FOO to, + but that expression gives the reader no hint of any information + concerning that datum. (LET ((BAR (HASH-TABLE/GET HASH-TABLE FOO + #F))) ...) gives a helpful name for the reader to understand the + code without having to find the definition of HASH-TABLE. + + Index variables such as i and j, or variables such as A and D naming + the car and cdr of a pair, are acceptable only if they are completely + unambiguous in the scope. For example, + + (do ((i 0 (+ i 1))) + ((= i (vector-length vector))) + (frobnicate (vector-ref vector i))) + + is acceptable because the scope of i is very clearly limited to a + single vector. However, if more vectors are involved, using more + index variables such as j and k will obscure the program further. + +Avoid functional combinators, or, worse, the point-free (or +`point-less') style of code that is popular in the Haskell world. At +most, use function composition only where the composition of functions +is the crux of the idea being expressed, rather than simply a procedure +that happens to be a composition of two others. + + Rationale: Tempting as it may be to recognize patterns that can be + structured as combinations of functional combinators -- say, `compose + this procedure with the projection of the second argument of that + other one', or (COMPOSE FOO (PROJECT 2 BAR)) --, the reader of the + code must subsequently examine the elaborate structure that has been + built up to obscure the underlying purpose. The previous fragment + could have been written (LAMBDA (A B) (FOO (BAR B))), which is in + fact shorter, and which tells the reader directly what argument is + being passed on to what, and what argument is being ignored, without + forcing the reader to search for the definitions of FOO and BAR or + the call site of the final composition. The explicit fragment + contains substantially more information when intermediate values are + named, which is very helpful for understanding it and especially for + modifying it later on. + + The screen space that can be potentially saved by using functional + combinators is made up for by the cognitive effort on the part of the + reader. The reader should not be asked to search globally for usage + sites in order to understand a local fragment. Only if the structure + of the composition really is central to the point of the narrative + should it be written as such. For example, in a symbolic integrator + or differentiator, composition is an important concept, but in most + code the structure of the composition is completely irrelevant to the + real point of the code. + +If a parameter is ignored, give it a meaningful name nevertheless and +say that it is ignored; do not simply call it `ignored'. + +In Common Lisp, variables can be ignored with (DECLARE (IGNORE ...)). +Some Scheme systems have similar declarations, but the portable way to +ignore variables is just to write them in a command context, where +their values will be discarded, preferably with a comment indicating +this purpose: + + (define (foo x y z) + x z ;ignore + (frobnitz y)) + + Rationale: As with using functional combinators to hide names, + avoiding meaningful names for ignored parameters only obscures the + purpose of the program. It is helpful for a reader to understand + what parameters a procedure is independent of, or if someone wishes + to change the procedure later on, it is helpful to know what other + parameters are available. If the ignored parameters were named + meaninglessly, then these people would be forced to search for call + sites of the procedure in order to get a rough idea of what + parameters might be passed here. + +When naming top-level bindings, assume namespace partitions unless in a +context where they are certain to be absent. Do not write explicit +namespace prefixes, such as FOO:BAR for an operation BAR in a module +FOO, unless the names will be used in a context known not to have any +kind of namespace partitions. + + Rationale: Explicit namespace prefixes are ugly, and lengthen names + without adding much semantic content. Common Lisp has its package + system to separate the namespaces of symbols; most Schemes have + mechanisms to do so as well, even if the RnRS do not specify any. It + is better to write clear names which can be disambiguated if + necessary, rather than to write names that assume some kind of + disambiguation to be necessary to begin with. Furthermore, explicit + namespace prefixes are inadequate to cover name clashes anyway: + someone else might choose the same namespace prefix. Relegating this + issue to a module system removes it from the content of the program, + where it is uninteresting. + +,** Comments + +Write comments only where the code is incapable of explaining itself. +Prefer self-explanatory code over explanatory comments. Avoid +`literate programming' like the plague. + + Rationale: If the code is often incapable of explaining itself, then + perhaps it should be written in a more expressive language. This may + mean using a different programming language altogether, or, since we + are talking about Lisp, it may mean simply building a combinator + language or a macro language for the purpose. `Literate programming' + is the logical conclusion of languages incapable of explaining + themselves; it is a direct concession of the inexpressiveness of the + computer language implementing the program, to the extent that the + only way a human can understand the program is by having it rewritten + in a human language. + +Do not write interface documentation in the comments for the +implementation of the interface. Explain the interface at the top of +the file if it is a single-file library, or put that documentation in +another file altogether. (See the `Documentation' section below if the +interface documentation comments grow too large for a file.) + + Rationale: A reader who is interested only in the interface really + should not need to read through the implementation to pick out its + interface; by putting the interface documentation at the top, not + only is such a reader's task of identifying the interface made + easier, but the implementation code can be more liberally commented + without fear of distracting this reader. To a reader who is + interested in the implementation as well, the interface is still + useful in order to understand what concepts the implementation is + implementing. + + Example: + + In this example of a single-file library implementing the skip list + data structure, the first page explains the purpose and dependencies + of the file (which are useful for anyone who intends to use it, even + though dependencies are really implementation details), and the next + few pages explain the usage of skip lists as implemented in that + file. On the first page of implementation, `Skip List Structure', + there are some comments of interest only to a reader who wishes to + understand the implementation; the same goes for the rest of the + file, none of which must a reader read whose interest is only in the + usage of the library. + +Avoid block comments (i.e. #| ... |#). Use S-expression comments (`#;' +in Scheme, with the expression to comment on the next line; `#+(OR)' or +`#-(AND)' in Common Lisp) to comment out whole expressions. Use blocks +of line comments for text. + + Rationale: Editor support for block comments is weak, because it + requires keeping a detailed intermediate parse state of the whole + buffer, which most Emacsen do not do. At the very least, #|| ... ||# + is better, because most Emacsen will see vertical bars as symbol + delimiters, and lose trying to read a very, very long symbol, if they + try to parse #| ... |#, whereas they will just see two empty symbols + and otherwise innocuous text between them if they try to parse #|| + ... ||#. In any case, in Emacs, `M-x comment-region RET', or `M-;' + (comment-dwim), is trivial to type. + + The only standard comments in Scheme are line comments. There are + SRFIs for block comments and S-expression comments, but support for + them varies from system to system. Expression comments are not hard + for editors to deal with because it is safe not to deal with them at + all; however, in Scheme S-expression comments, which are written by + prefixing an expression with `#;', the expression to be commented + should be placed on the next line. This is because editors that do + not deal with them at all may see the semicolon as the start of a + line comment, which will throw them off. Expression comments in + Common Lisp, however, are always safe. + + In Common Lisp, the two read-time optionals that are guaranteed to + ignore any form following them are `#+(OR)' and `#-(AND)'. `#+NIL' + is sometimes used in their stead, but, while it may appear to be an + obviously false optional, it actually is not. The feature + expressions are read in the KEYWORD package, so NIL is read not as + CL:NIL, i.e. the boolean false value, but as :NIL, a keyword symbol + whose name happens to be `NIL'. Not only is it not read as the + boolean false value, but it has historically been used to indicate a + feature that might be enabled -- in JonL White's New Implementation + of Lisp! However, the New Implementation of Lisp is rather old these + days, and unlikely to matter much...until Alastair Bridgewater writes + Nyef's Implementation of Lisp. + +,** Documentation + +On-line references and documentation/manuals are both useful for +independent purposes, but there is a very fine distinction between +them. Do not generate documentation or manuals automatically from the +text of on-line references. + + Rationale: /On-line references/ are quick blurbs associated with + objects in a running Lisp image, such as documentation strings in + Common Lisp or Emacs Lisp. These assume that the reader is familiar + with the gist of the surrounding context, but unclear on details; + on-line references specify the details of individual objects. + + /Documentation/ and /manuals/ are fuller, organized, and cohesive + documents that explain the surrounding context to readers who are + unfamiliar with it. A reader should be able to pick a manual up and + begin reading it at some definite point, perusing it linearly to + acquire an understanding of the subject. Although manuals may be + dominated by reference sections, they should still have sections that + are linearly readable to acquaint the reader with context. + +,** Round and Square Brackets + +Some implementations of Scheme provide a non-standard extension of the +lexical syntax whereby balanced pairs of square brackets are +semantically indistinguishable from balanced pairs of round brackets. +Do not use this extension. + + Rationale: Because this is a non-standard extension, it creates + inherently non-portable code, of a nature much worse than using a + name in the program which is not defined by the R5RS. The reason + that we have distinct typographical symbols in the first place is to + express different meaning. The only distinction between round + brackets and square brackets is in convention, but the precise nature + of the convention is not specified by proponents of square brackets, + who suggest that they be used for `clauses', or for forms that are + parts of enclosing forms. This would lead to such constructions as + + (let [(x 5) (y 3)] ...) + + or + + (let ([x 5] [y 3]) ...) + + or + + (let [[x 5] [y 3]] ...), + + the first two of which the author of this guide has seen both of, and + the last of which does nothing to help to distinguish the parentheses + anyway. + + The reader of the code should not be forced to stumble over a + semantic identity because it is expressed by a syntactic distinction. + The reader's focus should not be directed toward the lexical tokens; + it should be directed toward the structure, but using square brackets + draws the reader's attention unnecessarily to the lexical tokens. + +,* Attribution + +#+END_SRC +:END: + +**** TODO I have to get change (let ([x 5] [y 3])) -> (let ((x 5) (y 3))) +**** TODO comments + +#+BEGIN_SRC scheme + ;;;; Frob Grovel + + ;;; This section of code has some important implications: + ;;; 1. Foo. + ;;; 2. Bar. + ;;; 3. Baz. + + (define (fnord zarquon) + ;; If zob, then veeblefitz. + (quux zot + mumble ;Zibblefrotz. + frotz)) + +#+END_SRC +**** TODO literal data +Strings + +If the form in question is meant to be simply a list of literal data, +all of the subforms should be aligned to the same column, irrespective +of the first subform. + + Unacceptable: + + ("foo" "bar" "baz" "quux" "zot" + "mumble" "frotz" "gargle" "mumph") + + Questionable, but acceptable: + + (3 1 4 1 5 9 2 6 5 3 5 8 9 7 9 3 2 3 8 4 6 2 6 4 + 3 3 8 3 2 7 9 5 0 2 8 8 4 1 9 7 1 6 9 3 9 9 3) + + Acceptable: + + ("foo" "bar" "baz" "quux" "zot" + "mumble" "frotz" "gargle" "mumph") + + ("foo" + "bar" "baz" "quux" "zot" + "mumble" "frotz" "gargle" "mumph") +**** TODO follow this syntax convention proc args + +:alignment: +The operator of any form, i.e. the first subform following the opening +round bracket, determines the rules for indenting or aligning the +remaining forms. Many names in this position indicate special +alignment or indentation rules; these are special operators, macros, or +procedures that have certain parameter structures. + +If the first subform is a non-special name, however, then if the second +subform is on the same line, align the starting column of all following +subforms with that of the second subform. If the second subform is on +the following line, align its starting column with that of the first +subform, and do the same for all remaining subforms. + +In general, Emacs will indent Lisp code correctly. Run `C-M-q' +(indent-sexp) on any code to ensure that it is indented correctly, and +configure Emacs so that any non-standard forms are indented +appropriately. + + Unacceptable: + + (+ (sqrt -1) + (* x y) + (+ p q)) + + (+ + (sqrt -1) + (* x y) + (+ p q)) + + Acceptable: + + (+ (sqrt -1) + (* x y) + (+ p q)) + + (+ + (sqrt -1) + (* x y) + (+ p q)) + + Rationale: The columnar alignment allows the reader to follow the + operands of any operation straightforwardly, simply by scanning + downward or upward to match a common column. Indentation dictates + structure; confusing indentation is a burden on the reader who wishes + to derive structure without matching parentheses manually. + +:END: + +If you have a procedure, then it's arguments should be on the same line. +#+BEGIN_SRC scheme +(proc args + (proc (proc + args) + (proc args) + (proc (proc + args) + (proc (proc + (proc (proc + args)))))) + + (proc (proc + args))) +#+END_SRC + +*** TODO write various tests for == + +I have many bits of code in opensmtpd.org.archive that should result in an +error. I should write some tests for this. +** NO should I modifiy some of the records to include a sanitize field? +Probably not. It would be cool if this function ran automatically upon record +initiation, but there's no to make it do that. +** TODO which sanitize function is better? Pick the better sanitize method and use that one. + +The sanitize function found in opensmtpd-listen-on-configuration-filters + +Or the sanitize function +sanitize-list-of-options-for-match-configuration ? + +sanitize-list-of-options-for-match-configuration is probably faster. But is it? +It is an iteratize loop that checks for all issues as it loops through the +options. There is a lot of repetitive code in this procedure. + +BUT opensmtpd-listen-on-configuration-filters certainly seems easier to follow. +** TODO remove opensmtpd-table-type fieldname and instead move that it its own procedure outside of the record + +ONly use one function instead of + + +;; this procedure takes in one argument. +;; if that argument is an whose fieldname 'values' is an assoc-list, then it returns +;; #t, #f if otherwise. +;; TODO should I remove these two functions? And instead use the (opensmtpd-table-configuration-type) procedure? +(define (table-whose-data-are-assoc-list? table) + (if (not (opensmtpd-table-configuration? table)) + #f + (assoc-list? (opensmtpd-table-configuration-data table)))) + +;; this procedure takes in one argument +;; if that argument is an whose fieldname 'values' is a list of strings, then it returns +;; #t, #f if otherwise. +(define (table-whose-data-are-a-list-of-strings? table) + (if (not (opensmtpd-table-configuration? table)) + #f + (list-of-strings? (opensmtpd-table-configuration-data table)))) + + And opensmtpd-table-type +** TODO OpenSMTPD Service documentation + +OpenSMTPD is an easy-to-use mail transfer agent (MTA). Its configuration file is +throughly documented in man 5 =smtpd.conf=. OpenSMTPD *listens* for incoming +mail and *matches* the mail to *actions*. The following records represent those +stages: ~~, +~=, =~, +~~, and +~~. + +Additionally, each ~~ and +~~ may use a list of +~~, and/or +~~ records to filter email/spam. Also +numerous records' fieldnames use ~~ to hold lists +or key value pairs of data. + +A simple example configuration is below: + +#+BEGIN_SRC scheme +(let ((smtp.gnu.org (opensmtpd-pki-configuration + (domain "smtp.gnu.org") + (cert "file.cert") + (key "file.key")))) + (service opensmtpd-service-type + (opensmtpd-configuration + (listen-ons (list + (opensmtpd-listen-on-configuration + (pki smtp.gnu.org)) + (opensmtpd-listen-on-configuration + (pki smtp.gnu.org) + (secure-connection "smtps")))) + (matches (list + (opensmtpd-match-configuration + (action + (opensmtpd-action-local-delivery-configuration + (name "local-delivery")))) + (opensmtpd-match-configuration + (action + (opensmtpd-action-relay-configuration + (name "relay"))))))))) +#+END_SRC + +- Scheme Variable: opensmtpd-service-type + + Service type for the OpenSMTPD ([[https://www.opensmtpd.org][https://www.opensmtpd.org]]) email server. The + value for this service type is a ~~ record. + +- Data Type: opensmtpd-configuration + + Data type representing the configuration of OpenSMTPD. + + - ~package~ (default: ~opensmtpd~) + + The OpenSMTPD package to use. + + - ~config-file~ (default: ~#f~) + + File-like object of the OpenSMTPD configuration file to use. By default it + listens on the loopback network interface, and allows for mail from users + and daemons on the local machine, as well as permitting email to remote + servers. Run ~man smtpd.conf~ for more information. + + - ~bounce~ (default: ~(list "4h")~) + + ~bounce~ is a list of strings, which send warning messages to the envelope + sender when temporary delivery failures cause a message to remain in the + queue for longer than string _delay_. Each string _delay_ parameter consists + of a string beginning with a positive decimal integer and a unit s, m, h, + or d. At most four delay parameters can be specified. + + - ~listen-ons~ (default: ~(list (opensmtpd-listen-on-configuration))~ ) + + ~listen-ons~ is a list of ~~ records. + This list details what interfaces and ports OpenSMTPD listens on as well as + other information. + + - ~listen-on-socket~ (default: ~(opensmtpd-listen-on-socket-configuration-configuration)~ ) + + Listens for incoming connections on the Unix domain socket. + + - ~includes~ (default: ~#f~) + + # TODO ~includes~ should support a list of string filenames or gexps. + ~includes~ is a list of string _filenames_. Each filename's contents is + additional configuration that is inserted into the top of the configuration + file. + + - ~matches~ default: + + #+BEGIN_SRC scheme + (list (opensmtpd-match-configuration + (action (opensmtpd-action-local-delivery-configuration + (name "local") + (method "mbox"))) + (for (opensmtpd-option-configuration + (option "for local")))) + (opensmtpd-match-configuration + (action (opensmtpd-action-relay-configuration + (name "outbound"))) + (from (opensmtpd-option-configuration + (option "from local"))) + (for (opensmtpd-option-configuration + (option "for any"))))) + #+END_SRC + + ~matches~ is a list of ~~ records, which + matches incoming mail and sends it to a correspending action. The match + records are evaluated sequentially, with the first match winning. If an + incoming mail does not match any match records, then it is rejected. + + # TODO when the code supports mda-wrappers, add in this documentation. + # - ~mda-wrappers~ + + - ~mta-max-deferred~ (default: ~100~) + + When delivery to a given host is suspended due to temporary failures, cache + at most _number_ envelopes for that host such that they can be delivered as + soon as another delivery succeeds to that host. The default is 100. + + - ~queue~ (default: ~#f~) + + ~queue~ expects an ~~ record. With it, one may + compress and encrypt queue-ed emails as well as set the default expiration + time for temporarily undeliverable messages. + + - ~smtp~ (default: ~#f~) + + ~smtp~ expects an ~~ record, which lets one + specifiy how large email may be along with other settings. + + - ~srs~ (default: ~#f~) + + ~srs~ expects an ~~ record, which lets one set + up SRS, the Sender Rewritting Scheme. + +- Data Type: opensmtpd-listen-on-configuration + + Data type representing the configuration of an + ~~. Listen on the fieldname ~interface~ for + incoming connections, using the same syntax as for ifconfig(8). The interface + parameter may also be an string interface group, an string IP address, or a + string domain name. Listening can optionally be restricted to a specific + address fieldname ~family~, which can be either "inet4" or "inet6". + + - ~interface~ (default: "lo") + + The string interface to listen for incoming connections. These interface can + usually be found by the command ~ip link~. + + - ~family~ (default: ~#f~) + + The string IP family to use. Valid strings are "inet4" or "inet6". + + - ~auth~ (default: ~#f~) + + Support SMTPAUTH: clients may only start SMTP transactions after successful + authentication. If ~auth~ is ~#t~, then users are authenticated against + their own normal login credentials. Alternatively ~auth~ may be an + ~~ whose users are authenticated against + their passwords. + + - ~auth-optional~ (default: ~#f~) + + Support SMTPAUTH optionally: clients need not authenticate, but may do so. + This allows the ~~ to both accept + incoming mail from untrusted senders and permit outgoing mail from + authenticated users (using ~~ fieldname + ~auth~). It can be used in situations where it is not possible to listen on + a separate port (usually the submission port, 587) for users to + authenticate. + + - ~filters~ (default: ~#f~) + + A list of one or many ~~ or + ~~ records. The filters are applied + sequentially. These records listen and filter on connections handled by this + listener. + + - ~hostname~ (default: ~#f~) + + Use string "hostname" in the greeting banner instead of the default server + name. + + - ~hostnames~ (default: ~#f~) + + Override the server name for specific addresses. Use a + ~~ containing a mapping of string IP + addresses to hostnames. If the address on which the connection arrives + appears in the mapping, the associated hostname is used. + + - ~mask-src~ (default: ~#f~) + + If ~#t~, then omit the from part when prepending “Received” headers. + + - ~disable-dsn~ (default: ~#f~) + + When ~#t~, then disable the DSN (Delivery Status Notification) extension. + + - ~pki~ (default: ~#f~) + + For secure connections, use an ~~ + to prove a mail server's identity. + + - ~port~ (default: ~#f~) + + Listen on the _integer_ port instead of the default port of 25. + + - ~proxy-v2~ (default: ~#f~) + + If ~#t~, then support the PROXYv2 protocol, rewriting appropriately source + address received from proxy. + + - ~received-auth~ (default: ~#f~) + + If ~#t~, then in “Received” headers, report whether the session was + authenticated and by which local user. + + - ~senders~ (default: ~#f~) + + Look up the authenticated user in the supplied + ~~ to find the email addresses that user is + allowed to submit mail as. + + - ~secure-connection~ (default: ~#f~) + + This is a string of one of these options: + + |----------------------+---------------------------------------------| + | "smtps" | Support SMTPS, by default on port 465. | + |----------------------+---------------------------------------------| + | "tls" | Support STARTTLS, by default on port 25. | + |----------------------+---------------------------------------------| + | "tls-require-verify" | Like tls, but force clients to establish | + | | a secure connection before being allowed to | + | | start an SMTP transaction. With the verify | + | | option, clients must also provide a valid | + | | certificate to establish an SMTP session. | + |----------------------+---------------------------------------------| + + - ~tag~ (default: ~#f~) + + Clients connecting to the listener are tagged with the given string tag. + +- Data Type: opensmtpd-listen-on-socket-configuration + + Data type representing the configuration of an + ~~. Listen for incoming SMTP + connections on the Unix domain socket =/var/run/smtpd.sock=. This is done by + default, even if the directive is absent. + + - ~filters~ (default: ~#f~) + + A list of one or many ~~ or + ~~ records. These filter incoming + connections handled by this listener. + + - ~mask-src~ (default: ~#f~) + + If ~#t~, then omit the from part when prepending “Received” headers. + + - ~tag~ (default: ~#f~) + + Clients connecting to the listener are tagged with the given string tag. + +- Data Type: opensmtpd-match-configuration + + This data type represents the configuration of an + ~~ record. + + If at least one mail envelope matches the options of one match record, receive + the incoming message, put a copy into each matching envelope, and atomically + save the envelopes to the mail spool for later processing by the respective + ~~ found in fieldname ~action~. + + - ~action~ (default: ~#f~) + + If mail matches this match configuration, then do this action. Valid values + include ~~ or + ~~. + + - ~options~ (default: ~#f~) ~~ + The fieldname 'option' is a list of unique + ~~ records. + + Each ~~ record's fieldname 'option' has some + mutually exclusive options: there can be one "for" and one "from" option. + + |---------------------------+--------------------------------| + | for | from | + |---------------------------+--------------------------------| + | use one of the following: | only use one of the following: | + |---------------------------+--------------------------------| + | "for any" | "from any" | + | "for local" | "from auth" | + | "for domain" | "from local" | + | "for rcpt-to" | "from mail-from" | + | | "from socket" | + | | "from src" | + |---------------------------+--------------------------------| + + The following matching options are supported and can all be negated via (not + #t). The options that support a table (anything surrounded with '<' and '>' + eg:
), also support specifying regex via (regex #t). + + - =for any= + + Specify that session may address any destination. + + - =for local= + + Specify that session may address any local domain. This is the default, + and may be omitted. + + - =for domain _domain_ | = + + Specify that session may address the string or list table _domain_. + + - =for rcpt-to _recipient_ | = + + Specify that session may address the string or list table _recipient_. + + - =from any= + + Specify that session may originate from any source. + + - =from auth= + + Specify that session may originate from any authenticated user, no matter + the source IP address. + + - =from auth _user_ | = + + Specify that session may originate from authenticated _user_ or user list + user, no matter the source IP address. + + - =from local= + + Specify that session may only originate from a local IP address, or from + the local enqueuer. This is the default, and may be omitted. + + - =from mail-from _sender_ | = + + Specify that session may originate from _sender_ or table _sender_, no + matter the source IP address. + + - =from rdns= + + Specify that session may only originate from an IP address that resolves + to a reverse DNS. + + - =from rdns _hostname_ | = + + Specify that session may only originate from an IP address that resolves + to a reverse DNS matching string or list string _hostname_. + + - =from socket= + + Specify that session may only originate from the local enqueuer. + + - =from src _address_ |
= + + Specify that session may only originate from string or list table address + which can be a specific _address_ or a subnet expressed in CIDR-notation. + + - =auth= + + Matches transactions which have been authenticated. + + - =auth _username_ | = + + Matches transactions which have been authenticated for user or user list + _username_. + + - =helo _helo-name_ | = + + Specify that session's HELO / EHLO should match the string or list table + _helo-name_. + + - =mail-from _sender_ | = + + Specify that transactions's MAIL FROM should match the string or list + table _sender_. + + - =rcpt-to _recipient_ | = + + Specify that transaction's RCPT TO should match the string or list table + _recipient_. + + - =tag tag= + Matches transactions tagged with the given _tag_. + + - =tls= + Specify that transaction should take place in a TLS channel. + + Here is a simple example: + #+BEGIN_SRC scheme + (opensmtpd-option-configuration + (not #t) + (regex #f) + (option "for domain") + (data (opensmtpd-table-configuration + (name "domain-table") + (data (list "gnu.org" "dismail.de"))))) + #+END_SRC + + The mail must NOT come from the domains =gnu.org= or =dismail.de=. + + - Data Type: opensmtpd-option-configuration + +- Data Type: opensmtpd-action-local-delivery-configuration + + This data type represents the configuration of an + ~~ record. + + - ~name~ (default: ~#f~) + + ~name~ is the string name of the relay action. + + - ~method~ (default: ~"mbox"~) + + The email delivery option. Valid options are: + + - ~"mbox"~ + + Deliver the message to the user's mbox with mail.local(8). + + - ~"expand-only"~ + + Only accept the message if a delivery method was specified in an aliases + or _.forward file_. + + - ~"forward-only"~ + + Only accept the message if the recipient results in a remote address after + the processing of aliases or forward file. + + - ~~ + + Deliver the message to an LMTP server at + ~~'s fieldname ~destination~. The location + may be expressed as string host:port or as a UNIX socket. Optionally, + ~~'s fieldname ~rcpt-to~ might be specified + to use the recipient email address (after expansion) instead of the local + user in the LMTP session as RCPT TO. + + - ~~ + + Deliver the message to the maildir in + ~~'s fieldname ~pathname~ if specified, + or by default to =~/Maildir=. + + The pathname may contain format specifiers that are expanded before use + (see the below section about Format Specifiers). + + If ~~'s record fieldname ~junk~ is ~#t~, + then message will be moved to the ‘Junk’ folder if it contains a positive + ‘X-Spam’ header. This folder will be created under fieldname ~pathname~ if + it does not yet exist. + + - ~~ + + Delegate the delivery to the ~~'s fieldname + ~command~ (type string) that receives the message on its standard input. + + The ~command~ may contain format specifiers that are expanded before use + (see Format Specifiers). + + - ~alias~ (default: ~#f~) + + Use the mapping table for aliases expansion. ~alias~ is an + ~~. + + - ~ttl~ (default: ~#f~) + + ~ttl~ is a string specify how long a message may remain in the queue. It's + format is =n{s|m|h|d}=. eg: "4m" is four minutes. + + - ~user~ (default: ~#f~ ) + + ~user~ is the string username for performing the delivery, to be looked up + with getpwnam(3). + + This is used for virtual hosting where a single username is in charge of + handling delivery for all virtual users. + + This option is not usable with the mbox delivery method. + + - ~userbase~ (default: ~#f~) + + ~userbase~ is an ~~ record for mapping user + lookups instead of the getpwnam(3) function. + + The fieldnames ~user~ and ~userbase~ are mutually exclusive. + + - ~virtual~ (default: ~#f~) + + ~virtual~ is an ~~ record is used for virtual + expansion. + # TODO man 5 smtpd.conf says "The aliasing table format is described in + # table(5)." What is virtual expansion? I do NOT know how to use ~virtual~ + # properly. What sort of do I need? does the + # below work? + # (opensmtpd-table (name "virtual") (data '(("joshua" . "jbranso@dismail.de")))) + + # TODO fix this ~wrapper documentation~. Should it accept an + # ? If so, then I need to write an + # - ~wrapper~ (default: ) + + # TODO double check that these options are all correct + +- Data Type: opensmtpd-action-relay-configuration + + This data type represents the configuration of an + ~~ record. + + - ~name~ (default: ~#f~) + + ~name~ is the string name of the relay action. + + - ~backup~ (default: ~#f~) + + When ~#t~, operate as a backup mail exchanger delivering messages to any + mail exchanger with higher priority. + + - ~backup-mx~ (default: ~#f~) + + Operate as a backup mail exchanger delivering messages to any mail exchanger + with higher priority than mail exchanger identified as string name. + + - ~helo~ (default: ~#f~) + + Advertise string heloname as the hostname to other mail exchangers during + the HELO phase. + + - ~helo-src~ (default: ~#f~ ) + + Use the mapping ~~ to look up a hostname + matching the source address, to advertise during the HELO phase. + + - ~domain~ (default: ~#f~) + + Do not perform MX lookups but look up destination domain in an + ~~ and use matching relay url as relay host. + + - ~host~ (default: ~#f~) + + Do not perform MX lookups but relay messages to the relay host described by + the string relay-url. The format for relay-url is + =[proto://[label@]]host[:port]=. The following protocols are available: + + |------------+----------------------------------------------------------------| + | smtp | Normal SMTP session with opportunistic STARTTLS (the default). | + | smtp+tls | Normal SMTP session with mandatory STARTTLS. | + | smtp+notls | Plain text SMTP session without TLS. | + | lmtp | LMTP session. port is required. | + | smtps | SMTP session with forced TLS on connection, default port is | + | | 465. | + |------------+----------------------------------------------------------------| + + Unless noted, port defaults to 25. + + The label corresponds to an entry in a credentials table, as documented in + =table(5)=. It is used with the ="smtp+tls"= and ="smtps"= protocols for + authentication. Server certificates for those protocols are verified by + default. + + - ~pki~ (default: ~#f~) + + For secure connections, use the certificate associated with + ~~ (declared in a pki directive) to prove the + client's identity to the remote mail server. + + - ~srs~ (default: ~#f~) + + If ~#t~, then when relaying a mail resulting from a forward, use the Sender + Rewriting Scheme to rewrite sender address. + + - ~tls~ (default: ~#f~) boolean or string "no-verify" + + When ~#t~, Require TLS to be used when relaying, using mandatory STARTTLS by + default. When used with a smarthost, the protocol must not be + ="smtp+notls://"=. When string ~"no-verify"~, then do not require a valid + certificate. + + - ~auth~ (default: ~#f~) ~~ + + Use the alist ~~ for connecting to relay-url + using credentials. This option is usable only with fieldname ~host~ option. + + - ~mail-from~ (default: ~#f~) string + + Use the string _mailaddress_ as MAIL FROM address within the SMTP transaction. + + - ~src~ (default: ~#f~) string | ~~ + + Use the string or ~~ sourceaddr for the + source IP address, which is useful on machines with multiple interfaces. If + the list contains more than one address, all of them are used in such a way + that traffic is routed as efficiently as possible. + +- Data Type: opensmtpd-filter-configuration + + This data type represents the configuration of an + ~~. This is the filter record one should use + if they want to use an external package to filter email eg: rspamd or + spamassassin. + + - ~name~ (default: ~#f~) + + The string name of the filter. + + - ~proc~ (default: ~#f~) + + # TODO let ~proc~ be a gexp + The string command or process name. If ~proc-exec~ is ~#t~, ~proc~ is + treated as a command to execute. Otherwise, it is a process name. + + - ~proc-exec~ (default: ~#f~) + +- Data Type: opensmtpd-filter-phase-configuration + + This data type represents the configuration of an + ~~. + + In a regular workflow, smtpd(8) may accept or reject a message based only on + the content of envelopes. Its decisions are about the handling of the message, + not about the handling of an active session. + + Filtering extends the decision making process by allowing smtpd(8) to stop at + each phase of an SMTP session, check that options are met, then decide if a + session is allowed to move forward. + + With filtering via an ~~ record, a + session may be interrupted at any phase before an envelope is complete. A + message may also be rejected after being submitted, regardless of whether the + envelope was accepted or not. + + - ~name~ (default: ~#f~) + + The string name of the filter phase. + + - ~phase-name~ (default: ~#f~) + + The string name of the phase. Valid values are: + + |-------------+-----------------------------------------------| + | "connect" | upon connection, before a banner is displayed | + |-------------+-----------------------------------------------| + | "helo" | after HELO command is submitted | + |-------------+-----------------------------------------------| + | "ehlo" | after EHLO command is submitted | + |-------------+-----------------------------------------------| + | "mail-from" | after MAIL FROM command is submitted | + |-------------+-----------------------------------------------| + | "rcpt-to" | after RCPT TO command is submitted | + |-------------+-----------------------------------------------| + | "data" | after DATA command is submitted | + |-------------+-----------------------------------------------| + | "commit" | after message is fully is submitted | + |-------------+-----------------------------------------------| + + - ~options~ (default ~#f~) + + A list of unique ~~ records. + + At each phase, various options, specified by a list of + ~~, may be checked. The + ~~'s fieldname 'option' values of: "fcrdns", + "rdns", and "src" data are available in all phases, but other data must have + been already submitted before they are available. Options with a =
= + next to them require the ~~'s fieldname + ~data~ to be an ~~. There are the available + options: + + |-------------------+----------------------------------------| + | fcrdns | forward-confirmed reverse DNS is valid | + |-------------------+----------------------------------------| + | rdns | session has a reverse DNS | + |-------------------+----------------------------------------| + | rdns
| session has a reverse DNS in table | + |-------------------+----------------------------------------| + | src
| source address is in table | + |-------------------+----------------------------------------| + | helo
| helo name is in table | + |-------------------+----------------------------------------| + | auth | session is authenticated | + |-------------------+----------------------------------------| + | auth
| session username is in table | + |-------------------+----------------------------------------| + | mail-from
| sender address is in table | + |-------------------+----------------------------------------| + | rcpt-to
| recipient address is in table | + |-------------------+----------------------------------------| + + These conditions may all be negated by setting + ~~'s fieldname ~not~ to ~#t~. + + Any conditions that require a table may indicate that tables include regexs + setting ~~'s fieldname ~regex~ to ~#t~. + + - ~decision~ + + A string decision to be taken. Some decisions require an ~message~ or + ~value~. Valid strings are: + + |----------------------+------------------------------------------------| + | "bypass" | the session or transaction bypasses filters | + |----------------------+------------------------------------------------| + | "disconnect" message | the session is disconnected with message | + |----------------------+------------------------------------------------| + | "junk" | the session or transaction is junked, i.e., an | + | | ‘X-Spam: yes’ header is added to any messages | + |----------------------+------------------------------------------------| + | "reject" message | the command is rejected with message | + |----------------------+------------------------------------------------| + | "rewrite" value | the command parameter is rewritten with value | + |----------------------+------------------------------------------------| + + Decisions that involve a message require that the message be RFC valid, + meaning that they should either start with a 4xx or 5xx status code. + Descisions can be taken at any phase, though junking can only happen before + a message is committed. + + - ~message~ (default ~#f~) + + A string message beginning with a 4xx or 5xx status code. + + - ~value~ (default: ~#f~) + + A number value. ~value~ and ~message~ are mutually exclusive. + +- Data Type: opensmtpd-option-configuration + + This data type represents the configuration of an + ~~, which is used by + ~~ and ~~ + to match various options for email. + + - ~conditition~ (default ~#f~) + + A string option to be taken. Some options require a string or an + ~~ via the fieldname data. When the option + record is used inside of an ~~, then + valid strings are: + + At each phase, various options may be matched. The fcrdns, rdns, and src + data are available in all phases, but other data must have been already + submitted before they are available. + + |---------------------+----------------------------------------| + | "fcrdns" | forward-confirmed reverse DNS is valid | + | "rdns" | session has a reverse DNS | + | "rdns"
| session has a reverse DNS in table | + | "src"
| source address is in table | + | "helo"
| helo name is in table | + | "auth" | session is authenticated | + | "auth"
| session username is in table | + | "mail-from"
| sender address is in table | + | "rcpt-to"
| recipient address is in table | + |---------------------+----------------------------------------| + + When ~~ is used inside of an + ~~, then valid strigs for fieldname ~option~ + are: "for", "for any", "for local", "for domain", "for rcpt-to", "from any" + "from auth", "from local", "from mail-from", "from rdns", "from socket", + "from src", "auth", "helo", "mail-from", "rcpt-to", "tag", or "tls". + + - ~data~ (default ~#f~) ~~ + + Some options require a table to be present. One would specify that table + here. + - ~regex~ (default: ~#f~) boolean + + Any options using a table may indicate that tables hold regex by + prefixing the table name with the keyword regex. + - ~not~ (default: ~#f~) boolean + + When ~#t~, this option record is negated. + +- Data Type: opensmtpd-table-configuration + + This data type represents the configuration of an + ~~. + + - ~name~ (default ~#f~) + + ~name~ is the name of the ~~ record. + + - ~data~ (default: ~#f~) + + ~data~ expects a list of strings or an alist, which is a list of + cons cells. eg: ~(data (list ("james" . "password")))~ OR + ~(data (list ("gnu.org" "fsf.org")))~. + +- Data Type: opensmtpd-pki-configuration + + This data type represents the configuration of an + ~~. + + - ~domain~ (default ~#f~) + + ~domain~ is the string name of the ~~ record. + + - ~cert~ (default: ~#f~) + + ~cert~ (default: ~#f~) + + ~cert~ is the string certificate filename to use for this pki. + + - ~key~ (default: ~#f~) + + ~key~ is the string certificate falename to use for this pki. + + - ~dhe~ (default: ~"none"~) + + Specify the DHE string parameter to use for DHE cipher suites with host + pkiname. Valid parameter values are "none", "legacy", or "auto". For "legacy", a + fixed key length of 1024 bits is used, whereas for "auto", the key length is + determined automatically. The default is "none", which disables DHE cipher + suites. + +- Data Type: opensmtpd-maildir-configuration + + - ~pathname~ (default: ~"~/Maildir"~) + + Deliver the message to the maildir if pathname if specified, or by default + to =~/Maildir=. + + The pathname may contain format specifiers that are expanded before use + (see FORMAT SPECIFIERS). + + - ~junk~ (default: ~#f~) + + If the junk argument is ~#t~, then the message will be moved to the =‘Junk’= + folder if it contains a positive =‘X-Spam’= header. This folder will be + created under pathname if it does not yet exist. + +- Data Type: opensmtpd-mda-configuration + # Do we need a dataypte for mda configuration? + # this could just be a gexp in the fieldname opensmtpd-configuration-mda + + - ~name~ + + The string name for this MDA command. + + - ~command~ + + Delegate the delivery to a command that receives the message on its standard + input. + + The command may contain format specifiers that are expanded before use (see + FORMAT SPECIFIERS). + +- Data Type: opensmtpd-queue-configuration + + - ~compression~ (default ~#f~) + + Store queue files in a compressed format. This may be useful to save disk + space. + - ~encryption~ (default ~#f~) + + Encrypt queue files with EVP_aes_256_gcm(3). If no key is specified, it is + read with getpass(3). If the string stdin or a single dash (‘-’) is given + instead of a key, the key is read from the standard input. + - ~ttl-delay~ (default ~#f~) + + Set the default expiration time for temporarily undeliverable messages, + given as a positive decimal integer followed by a unit s, m, h, or d. The + default is four days ("4d"). + +- Data Type: opensmtpd-smtp-configuration + + Data type representing an ~~ record. + + - ~ciphers~ (default: ~#f~) + + Set the control string for SSL_CTX_set_cipher_list(3). The default is + "HIGH:!aNULL:!MD5". + - ~limit-max-mails~ (default: ~100~) + + Limit the number of messages to count for each sessio + - ~limit-max-rcpt~ (default: ~1000~) + + Limit the number of recipients to count for each transaction. + - ~max-message-size~ (default: ~35M~) + + Reject messages larger than size, given as a positive number of bytes or as + a string to be parsed with scan_scaled(3). + - ~sub-addr-delim character~ (default: ~+~) + + When resolving the local part of a local email address, ignore the ASCII + character and all characters following it. This is helpful for email + filters. ="admin+bills@gnu.org"= is the same email address as + ="admin@gnu.org"=. BUT an email filter can filter emails addressed to first + email address into a 'Bills' email folder. + +- Data Type: opensmtpd-srs-configuration + + - ~key~ (default: ~#f~) + + Set the secret key to use for SRS, the Sender Rewriting Scheme. + + - ~backup-key~ (default: ~#f~) + + Set a backup secret key to use as a fallback for SRS. This can be used to + implement SRS key rotation. + - ~ttl-delay~ (default: ~"4d"~) + + Set the time-to-live delay for SRS envelopes. After this delay, a bounce + reply to the SRS address will be discarded to limit risks of forged + addresses. + +- Format Specifiers + + Some configuration records support expansion of their parameters at + runtime. Such records (for example + ~~, ~~) may use + format specifiers which are expanded before delivery or relaying. The + following formats are currently supported: + + |---------------------+-------------------------------------------------------| + | =%{sender}= | sender email address, may be empty string | + | =%{sender.user}= | user part of the sender email address, may be empty | + | =%{sender.domain}= | domain part of the sender email address, may be empty | + | =%{rcpt}= | recipient email address | + | =%{rcpt.user}= | user part of the recipient email address | + | =%{rcpt.domain}= | domain part of the recipient email address | + | =%{dest}= | recipient email address after expansion | + | =%{dest.user}= | user part after expansion | + | =%{dest.domain}= | domain part after expansion | + | =%{user.username}= | local user | + | =%{user.directory}= | home directory of the local user | + | =%{mbox.from}= | name used in mbox From separator lines | + | =%{mda}= | mda command, only available for mda wrappers | + |---------------------+-------------------------------------------------------| + + Expansion formats also support partial expansion using the optional bracket notations + with substring offset. For example, with recipient domain =“example.org”=: + + |------------------------+----------------------| + | =%{rcpt.domain[0]}= | expands to “e” | + | =%{rcpt.domain[1]}= | expands to “x” | + | =%{rcpt.domain[8:]}= | expands to “org” | + | =%{rcpt.domain[-3:]}= | expands to “org” | + | =%{rcpt.domain[0:6]}= | expands to “example” | + | =%{rcpt.domain[0:-4]}= | expands to “example” | + |------------------------+----------------------| + + In addition, modifiers may be applied to the token. For example, with recipient + =“User+Tag@Example.org”=: + + |--------------------------+-----------------------------------| + | =%{rcpt:lowercase}= | expands to “user+tag@example.org” | + | =%{rcpt:uppercase}= | expands to “USER+TAG@EXAMPLE.ORG” | + | =%{rcpt:strip}= | expands to “User@Example.org” | + | =%{rcpt:lowercasestrip}= | expands to “user@example.org” | + |--------------------------+-----------------------------------| + + For security concerns, expanded values are sanitized and potentially dangerous + characters are replaced with ‘:’. In situations where they are desirable, the + “raw” modifier may be applied. For example, with recipient + =“user+t?g@example.org”=: + + |---------------+-----------------------------------| + | =%{rcpt}= | expands to “user+t:g@example.org” | + | =%{rcpt:raw}= | expands to “user+t?g@example.org” | + |---------------+-----------------------------------| +*** some example ~~ that are probably out of date + + #+BEGIN_SRC scheme + +;;this works! (opensmtpd-configuration->mixed-text-file (opensmtpd-configuration (smtp (opensmtpd-smtp-configuration (limit-max-rcpt 10))))) + +;; (tables (list +;; (opensmtpd-table-configuration +;; (name "aliases") +;; (data +;; (list +;; (cons "webmaster" "root") +;; (cons "postmaster" "root") +;; (cons "abuse" "root")))) +;; +;; (opensmtpd-table-configuration +;; (name "vdoms") +;; (data (list "gnucode.me" +;; "gnu-hurd.com"))) +;; (opensmtpd-table-configuration +;; (name (opensmtpd-table-configuration +;; (name "virtual") +;; (data (list "root" "postmaster@gnu.org")))) +;; (data (list (cons "joshua@gnucode.me" "joshua") +;; (cons "jbranso@gnucode.me" "joshua") +;; (cons "postmaster@gnucode.me" "joshua")))))) + + ;; (filter-chains + ;; (list + ;; (opensmtpd-filter-chain + ;; (name "dropDumbEmails") + ;; (filter-names (list "nofcrdnsDisconnect" + ;; "nordnsDisconnect"))))) + ;; (filter-phases + ;; (list (opensmtpd-filter-phase-configuration + ;; (name "nofcrdnsDisconnect") + ;; (phase-name "connect") + ;; (options (list "!fcrdns")) + ;; (decision "disconnect") + ;; (message "You have not set up forward confirmed DNS.")) + ;; (opensmtpd-filter-phase-configuration + ;; (name "nordnsDisconnect") + ;; (phase-name "connect") + ;; (options (list "!rdns")) + ;; + ;; (decision "reject") + ;; (message "You have not set up reverse DNS.")))) + ;; +(define example-opensmtpd-config-smaller + (opensmtpd-configuration + (listen-ons + (list + ;; this forum help suggests that I listen on 0.0.0.0 and NOT eth0 + ;; https://serverfault.com/questions/726795/opensmtpd-wont-work-at-reboot + ;; this listens for email from the outside world + ;; this lets local users logged into the system via ssh send email + (opensmtpd-listen-on-configuration + (interface "wlp2s0") + (port 465)))) + (matches (list + (opensmtpd-match-configuration + (name "maildir") + (action (opensmtpd-action-local-delivery-configuration + (method (opensmtpd-maildir-configuration + (pathname "/home/%{rcpt.user}/Maildir") + (junk #t))) + (virtual (opensmtpd-table-configuration + (name "virtual") + (data (list "root" "james@gnu.org")))))) + (for (opensmtpd-option-configuration + (option "for local")))))))) + +(define example-opensmtpd-config-small + (let ([interface "wlp2s0"] + [creds (opensmtpd-table-configuration + (name "creds") + (data + (list + (cons "joshua" + "$6$Ec4m8FgKjT2F/03Y$k66ABdse9TzCX6qaALB3WBL9GC1rmAWJmaoSjFMpbhzat7DOpFqpnOwpbZ34wwsQYIK8RQlqwM1I/v6vsRq86."))))] + [receive-action (opensmtpd-action-local-delivery-configuration + (name "receive") + (method (opensmtpd-maildir-configuration + (pathname "/home/%{rcpt.user}/Maildir") + (junk #t))) + (virtual (opensmtpd-table-configuration + (name "virtual") + (data (list "root" "james@gnu.org")))))] + [smtp.gnucode.me (opensmtpd-pki-configuration + (domain "smtp.gnucode.me") + (cert "opensmtpd.scm") + (key "opensmtpd.scm"))]) + (opensmtpd-configuration + (listen-ons + (list + ;; this forum help suggests that I listen on 0.0.0.0 and NOT eth0 + ;; https://serverfault.com/questions/726795/opensmtpd-wont-work-at-reboot + ;; this listens for email from the outside world + (opensmtpd-listen-on-configuration + (interface interface) + (port 25) + (secure-connection "tls") + (pki smtp.gnucode.me)) + ;; this lets local users logged into the system via ssh send email + (opensmtpd-listen-on-configuration + (interface interface) + (port 465) + (secure-connection "smtps") + (pki smtp.gnucode.me) + (auth creds)))) + (matches (list + (opensmtpd-match-configuration + (action receive-action) + (for (opensmtpd-option-configuration + (option "for local"))))))))) + +(define example-opensmtpd-config + (let ([interface "lo"] + [creds (opensmtpd-table-configuration + (name "creds") + (data + (list + (cons "joshua" + "$6$Ec4m8FgKjT2F/03Y$k66ABdse9TzCX6qaALB3WBL9GC1rmAWJmaoSjFMpbhzat7DOpFqpnOwpbZ34wwsQYIK8RQlqwM1I/v6vsRq86."))))] + [receive-action (opensmtpd-action-local-delivery-configuration + (name "receive") + (method (opensmtpd-maildir-configuration + (pathname "/home/%{rcpt.user}/Maildir") + (junk #t))) + (virtual (opensmtpd-table-configuration + (name "virtual") + (data (list "josh" "jbranso@dismail.de")))))] + [smtp.gnucode.me (opensmtpd-pki-configuration + (domain "smtp.gnucode.me") + (cert "opensmtpd.scm") + (key "opensmtpd.scm"))]) + (opensmtpd-configuration + ;; (mta-max-deferred 50) + ;; (queue + ;; (opensmtpd-queue-configuration + ;; (compression #t))) + ;; (smtp + ;; (opensmtpd-smtp-configuration + ;; (max-message-size "10M"))) + ;; (srs + ;; (opensmtpd-srs-configuration + ;; (ttl-delay "5d"))) + (listen-ons + (list + ;; this forum help suggests that I listen on 0.0.0.0 and NOT eth0 + ;; https://serverfault.com/questions/726795/opensmtpd-wont-work-at-reboot + ;; this listens for email from the outside world + (opensmtpd-listen-on-configuration + (interface interface) + (port 25) + (secure-connection "tls") + (pki smtp.gnucode.me)) + ;; this lets local users logged into the system via ssh send email + (opensmtpd-listen-on-configuration + (interface "lo") + (port 25) + (secure-connection "tls") + (pki smtp.gnucode.me)) + (opensmtpd-listen-on-configuration + (interface interface) + (port 465) + (secure-connection "smtps") + (pki smtp.gnucode.me) + (auth creds) + ;;(filter ) + ) + (opensmtpd-listen-on-configuration + (interface interface) + (port 587) + (secure-connection "tls-require") + (pki smtp.gnucode.me) + (auth creds)))) + (matches (list + (opensmtpd-match-configuration + (action (opensmtpd-action-relay-configuration + (name "send"))) + (for (opensmtpd-option-configuration + (option "for any"))) + (from (opensmtpd-option-configuration + (option "from any"))) + (auth (opensmtpd-option-configuration + (option "auth")))) + (opensmtpd-match-configuration + (action receive-action) + (from (opensmtpd-option-configuration + (option "from any"))) + (for (opensmtpd-option-configuration + (option "for domain") + (value (list "gnucode.me" "gnu-hurd.com"))))) + (opensmtpd-match-configuration + (action receive-action) + (for (opensmtpd-option-configuration + (option "for local"))))))))) + #+END_SRC + +*** some example smtpd.conf configs +*** serving multiple domains with one pki + +source: https://www.reddit.com/r/openbsd/comments/n41wkz/how_to_host_different_domains_for_an_email_server/ +#+BEGIN_EXAMPLE +​pki mail.primary.domain cert​pki mail.primary.domain cert "/etc/ssl/mail.primary.domain.fullchain.pem" + +pki mail.primary.domain key "/etc/ssl/private/mail.primary.domain.key" + + +filter check_dyndns phase connect match rdns regex { '.*\.dyn\..*', '.*\.dsl\..*' } \ + +disconnect "550 no residential connections" + + +filter check_rdns phase connect match !rdns \ + +disconnect "550 no rDNS is so 80s" + + +filter check_fcrdns phase connect match !fcrdns \ + +disconnect "550 no FCrDNS is so 80s" + + +filter senderscore \ + +proc-exec "filter-senderscore -blockBelow 10 -junkBelow 70 -slowFactor 5000" + + +filter rspamd proc-exec "filter-rspamd" + + +table usermap file:/etc/mail/usermap + +table credentials file:/etc/mail/credentials + +table domains { primary.domain, second.domain } + + +listen on all tls pki mail.primary.domain \ + +filter { check_dyndns, check_rdns, check_fcrdns, senderscore, rspamd } + + +listen on egress port 465 smtps pki mail.primary.domain \ + +auth ~~ filter rspamd + + +action "inbound" lmtp "/var/dovecot/lmtp" rcpt-to virtual ~~ #maildir junk alias + +action "outbound" relay helo mail.primary.domain + + +match from any for domain ~~ action "inbound" + +match for local action "inbound" + + +match from any auth for any action "outbound" + +match for any action "outbound" "/etc/ssl/mail.primary.domain.fullchain.pem" + +pki mail.primary.domain key "/etc/ssl/private/mail.primary.domain.key" + + +filter check_dyndns phase connect match rdns regex { '.*\.dyn\..*', '.*\.dsl\..*' } \ + +disconnect "550 no residential connections" + + +filter check_rdns phase connect match !rdns \ + +disconnect "550 no rDNS is so 80s" + + +filter check_fcrdns phase connect match !fcrdns \ + +disconnect "550 no FCrDNS is so 80s" + + +filter senderscore \ + +proc-exec "filter-senderscore -blockBelow 10 -junkBelow 70 -slowFactor 5000" + + +filter rspamd proc-exec "filter-rspamd" + + +table usermap file:/etc/mail/usermap + +table credentials file:/etc/mail/credentials + +table domains { primary.domain, second.domain } + + +listen on all tls pki mail.primary.domain \ + +filter { check_dyndns, check_rdns, check_fcrdns, senderscore, rspamd } + + +listen on egress port 465 smtps pki mail.primary.domain \ + +auth ~~ filter rspamd + + +action "inbound" lmtp "/var/dovecot/lmtp" rcpt-to virtual ~~ #maildir junk alias + +action "outbound" relay helo mail.primary.domain + + +match from any for domain ~~ action "inbound" + +match for local action "inbound" + + +match from any auth for any action "outbound" + +match for any action "outbound" + +#+END_EXAMPLE +** PROJ nice things to have [0/9] +*** TODO Should I delete ~~ ? or fieldname 'opensmtpd-configuration-mda-wrapppers'? + +~~'s fieldname 'method' allows +for an mda configuration. BUT instead of an mda-configuration record, you could +just use a list of strings and/or gexps. + +#+BEGIN_EXAMPLE + mda wrapper name command + Associate command with the mail delivery agent wrapper named name. When a local + delivery specifies a wrapper, the command associated with the wrapper will be ex‐ + ecuted instead. The command may contain format specifiers (see FORMAT + SPECIFIERS). +#+END_EXAMPLE + +If I choose to NOT delete ~~, then should I delete +'opensmtpd-configuration-mda-wrapppers'? + +Also should I delete the opensmtpd-action-local-delivery-configuration-wrapper? + +*** TODO make the 'auth-optional' and 'auth' fieldnames for ~~ autoencrypt passwords. [0/0] +Guix makes it pretty hard to find the openbsd binary file that encrypts +passwords for you. If I can progmatically find this file, it would be nice to +autoencrypt the users's passwords for you. + +What does this mean practically? + +#+BEGIN_SRC scheme +(opensmtpd-table-configuration + (name "credentials") + (data '(("joshua@gnu.org" . "somePassword") + ("postmaster@gnu.org") . "anotherSillyPassword"))) +#+END_SRC + +Gets stored in /gnu/store/ in the =smtpd.conf= as something like: + +#+BEGIN_EXAMPLE +table credentials { joshua@gnu.org = $some$Long$EncrytpedPassword, \ + postmaster@gnu.org = $some$Long$Other$EncrytpedPassword } +#+END_EXAMPLE + +You would need to encourage users NOT to have passwords in a public git repo. +With guile-git, it might be possible to sanitize the config, to ensure that the +passwords are NOT stored in the git repo. + +Alternatively, we could put the following in the documentation: + +#+BEGIN_SRC scheme +(use-modules (passwords)) + +(opensmtpd-table-configuration + (name "credentials") + (data %passwords)) +#+END_SRC + +*** PROJ Why does (opensmtpd-configuration) take so long to initialize? [0/1] + +For example, try to initialize this bit of code. It takes almost 5 seconds. +#+BEGIN_SRC scheme +(let ([interface "lo"] + [creds-table (opensmtpd-table-configuration + (name "creds") + (data + (list + (cons "joshua" + "$6$Ec4m8FgKjT2F/03Y$k66ABdse9TzCX6qaALB3WBL9GC1rmAWJmaoSjFMpbhzat7DOpFqpnOwpbZ34wwsQYIK8RQlqwM1I/v6vsRq86."))))] + [receive-action (opensmtpd-action-local-delivery-configuration + (name "receive") + (method (opensmtpd-maildir-configuration + (pathname "/home/%{rcpt.user}/Maildir") + (junk #t))) + (virtual (opensmtpd-table-configuration + (name "virtual") + (data (list "josh" "jbranso@dismail.de")))))] + [filter-dkimsign (opensmtpd-filter + (name "dkimsign") + (exec #t) + (proc (string-append "/path/to/dkimsign -d gnucode.me -s 2021-09-22 -c relaxed/relaxed -k " + "/path/to/dkimsign-key user nobody group nobody")))] + [smtp.gnucode.me (opensmtpd-pki-configuration + (domain "smtp.gnucode.me") + (cert "opensmtpd.scm") + (key "opensmtpd.scm"))]) + (opensmtpd-configuration + (mta-max-deferred 50) + (queue + (opensmtpd-queue-configuration + (compression #t))) + (smtp + (opensmtpd-smtp-configuration + (max-message-size "10M"))) + (srs + (opensmtpd-srs-configuration + (ttl-delay "5d"))) + (listen-ons + (list + (opensmtpd-listen-on-configuration + (interface interface) + (port 25) + (secure-connection "tls") + (filters (list (opensmtpd-filter-phase-configuration + (name "noFRDNS") + (phase "commit") + (options (list (opensmtpd-option-configuration + (option "fcrdns") + (not #t)))) + (decision "disconnect") + (message "No FCRDNS")))) + (pki smtp.gnucode.me)) + ;; this lets local users logged into the system via ssh send email + (opensmtpd-listen-on-configuration + (interface interface) + (port 465) + (secure-connection "smtps") + (pki smtp.gnucode.me) + (auth creds-table) + (filters (list filter-dkimsign))) + (opensmtpd-listen-on-configuration + (interface interface) + (port 587) + (secure-connection "tls-require") + (pki smtp.gnucode.me) + (auth creds-table) + (filters (list filter-dkimsign))))) + (matches (list + (opensmtpd-match-configuration + (action (opensmtpd-action-relay-configuration + (name "relay"))) + (for (opensmtpd-option-configuration + (option "for any"))) + (from (opensmtpd-option-configuration + (option "from any"))) + (auth (opensmtpd-option-configuration + (option "auth")))) + (opensmtpd-match-configuration + (action receive-action) + (from (opensmtpd-option-configuration + (option "from any"))) + (for (opensmtpd-option-configuration + (option "for domain") + (value (opensmtpd-table-configuration + (name "domain-table") + (data (list "gnucode.me" "gnu-hurd.com"))))))) + (opensmtpd-match-configuration + (action receive-action) + (for (opensmtpd-option-configuration + (option "for local")))))))) +#+END_SRC +**** TODO one area to look for speed up improvements would be in the sanitize function of =(opensmtpd-listen-on-configuration-filters)=. +*** PROJ check the code base for places to use apply, map, fold, eval, or remove [3/4] + +**** DONE string-in-list would be a good place. maybe is-value-right-type + +#+BEGIN_SRC scheme +(define (string-in-list? string list) + (if (null? list) + #f + (if (and (string? (car list)) (string=? string (car list))) + #t + (string-in-list? string (cdr list))))) + +(define (string-in-list? string list) + (primitive-eval (cons 'or (map (lambda (var) (string=? string var)) list)))) + +#+END_SRC + +**** DONE contains-duplicate +#+BEGIN_SRC scheme +(define (contains-duplicate? list) + (if (null? list) + #f + (or + ;; + (let loop ([list (cdr list)] + [1st (car list)]) + (if (null? list) + #f + (if (equal? 1st (car list)) + (data #t 1st) + (loop (cdr list) 1st)))) + ;; + (contains-duplicate? (cdr list))))) + +(define (contains-duplicate? list) + (if (null? list) + #f + (or (primitive-eval (cons 'or ; check if (car list) is in (cdr list) + (map (lambda (var) (equal? var (car list))) + (cdr list)))) + ;; check if (cdr list) contains duplicate + (contains-duplicate? (cdr list))))) +#+END_SRC + +**** DONE using remove and flatten and map +#+BEGIN_SRC scheme +(define (get-opensmtpd-table-configurations value) + (delete-duplicates + (let loop ([list (flatten + (cond ((opensmtpd-table-configuration? value) + value) + ((record? value) + (let* ([ (record-type-descriptor value)] + [list-of-record-fieldnames (record-type-fields )]) + (map (lambda (fieldname) + (get-opensmtpd-table-configurations ((record-accessor fieldname) value))) + list-of-record-fieldnames))) + ((and (list? value) (not (null? list))) + (map (lambda (element-in-list) + (if (record? element-in-list) + (get-opensmtpd-table-configurations element-in-list) + #f)) + value))))]) + (if (null? list) + '() + (if (opensmtpd-table-configuration? (car list)) + (cons (car list) (loop (cdr list))) + (loop (cdr list))))))) + +(define (get-opensmtpd-table-configurations value) + (let loop ([list (flatten ;; turn (list '(1) '(2 '(3))) -> '(1 2 3) + (cond ((opensmtpd-table-configuration? value) + value) + ((record? value) + (let* ([ (record-type-descriptor value)] + [list-of-record-fieldnames (record-type-fields )]) + (map (lambda (fieldname) + (get-opensmtpd-table-configurations ((record-accessor fieldname) value))) + list-of-record-fieldnames))) + ((and (list? value) (not (null? list))) + (map (lambda (element-in-list) + (if (record? element-in-list) + (get-opensmtpd-table-configurations element-in-list) + #f)) + value))))]) + (delete-duplicates (partition opensmtpd-table-configuration? list)))) + +#+END_SRC +**** TODO using map, apply, and fold is certainly awesome, but is it less efficient? + +For example, list-of-type? using a named let is pretty efficient. It loops +through the list once. + +#+BEGIN_SRC scheme +(define (list-of-type? list proc?) + (if (and (list? list) + (not (null? list))) + (let loop ([list list]) + (if (null? list) + #t + (if (proc? (car list)) + (loop (cdr list)) + #f))) + #f)) +#+END_SRC + +BUT when I using map on this, it is slightly less efficient. It has to apply a +simple procedure to each element in the list. Then it has to return the list of +booleans. Then it has to build the primitive eval list, then it has to eval it. +#+BEGIN_SRC scheme +(define (list-of-type? list proc?) + (if (and (list? list) + (not (null? list))) + (primitive-eval (cons 'and + (map (lambda (var) + (if (proc? var) + #t + #f)) + list))) + #f)) +#+END_SRC + +*** PROJ improve [0/2] +**** TODO it would be nice if ~~ supported aliasing tables, as described in man 5 table + +#+BEGIN_EXAMPLE + Aliasing tables + Aliasing tables are mappings that associate a recipient to one or many destinations. They can be + used in two contexts: primary domain aliases and virtual domain mapping. + + action name method alias
+ action name method virtual
+ + In a primary domain context, the key is the user part of the recipient address, whilst the value + is one or many recipients as described in aliases(5): + + user1 otheruser + user2 otheruser1,otheruser2 + user3 otheruser@example.com + + In a virtual domain context, the key is either a user part, a full email address or a catch all, + following selection rules described in smtpd.conf(5), and the value is one or many recipients as + described in aliases(5): + + user1 otheruser + user2@example.org otheruser1,otheruser2 + @example.org otheruser@example.com + @ catchall@example.com + +#+END_EXAMPLE + +Currently opensmtpd-table-configuration, does not support mapping a user to 5 email addresses. +For example, if user 'dave' can email as 'postmaster@gnu.org', and +'other@gnu.org', and 5 other email addresses... does not +support this kind of mapping. To support it, I may be able to just embed a +table in smtpd.conf, or I may need to create an /etc/aliases table as man 5 +aliases describes. +**** TODO make an with file-db #t, auto convert the table into a berkley database via makemap + +See man 5 table +and man smtpd.conf +*** TODO writing out the pkis when there are no pkis gives the string "\n"...it might be better to give "" instead + +=(opensmtpd-configuration-fieldname->string example-opensmtpd-with-0-pkis opensmtpd-configuration-pkis opensmtpd-pki-configuration->string)= +*** PROJ Can I make some improvements to my/sanitize procedure? [0/5] +**** how does my/sanitize procedure work? + +~(my/sanitize var "record-name" "fieldname" '(string? boolean? number?)')~ + +It is essentially asking? are you any of the following: string?, boolean?, +number? If not, then error out with a helpful error message. + +**** How does my hard-coded sanitized procedure work? eg [[file:opensmtpd-records.scm::(filters opensmtpd-listen-on-configuration-filters][opensmtpd-listen-on-configuration-filters]] + +This hard coded sanitize is a little different than the my/sanitize procedure. I +designed the thunk ~my/sanitize~, such that each thunk (string?, false?, +boolean?) has a corresponding entry in the procedure [[file:opensmtpd-records.scm::define (list-of-procedures->string + procedures][~(list-of-procedures->string procedures)~]]. + +However, it would be nice to have the sanitize invocation in +opensmtpd-listen-on-configuration-filters use a my/sanitize invocation like so. + +#+BEGIN_SRC scheme +(my/sanitize var "opensmtpd-listen-on-configuration" "filters" + (list false? + '(list-has-duplicates-or-non-filters + "is a list in which each unique element is of type \n" + "or .") + '(some-filters-in-list-need-message? + " fieldname: 'decision' options " + "\"disconnect\" and \"reject\" require fieldname 'message'\n" + "to have a string.\n") + '(some-filters-in-list-need-value? + " fieldname: 'decision' option " + "\"rewrite\" requires fieldname 'value'\n" + "to have a string.\n"))) +#+END_SRC + +**** PROJ better error messages for my/sanitize calls that use a lambda instead of a defined function [0/4] + +THIS IS HARD TO DO... NOT DOING IT! I just chose to use a hard-coded error +message baked into the lambda. I tried making my/sanitize better...but I could +not get it to work. The hard-coded method just works: + +#+BEGIN_SRC scheme +(phase-name opensmtpd-filter-phase-configuration-phase-name ;; string + (default #f) + (sanitize (lambda (var) + (if (and (string? var) + (or (string=? "connect" var) + (string=? "helo" var) + (string=? "mail-from" var) + (string=? "rcpt-to" var) + (string=? "data" var) + (string=? "commit" var))) + var + (begin + (display (string-append " fieldname: 'phase-name' is of type " + "string. The string can be either 'connect'," + " 'helo', 'mail-from', 'rcpt-to', 'data', or 'commit.'\n ")) + (throw 'bad! var)))))) +#+END_SRC + +Why? ~~ fieldnames only accept certain strings. I want to +sanitize each fieldname to make sure that it's strings is one of those strings. +How would I do this? + +For example, ~~ fieldname 'decision' uses +a lambda to sanitize itself. This will result in an error message that is +descriptive enough to solve the problem. If I decide to do this, then I probably +should create a non-exported record. + +#+BEGIN_SRC scheme +(opensmtpd-filter-phase-configuration (name "cat") (phase-name "connect") (options "fcrdns") (decision "bypasse")) +#+END_SRC + +#+RESULTS: +: (opensmtpd-filter-phase-configuration (name "cat") (phase-name "connect") (options "fcrdns") (decision "bypasse")) +: fieldname 'bypasse' is of type. +: #:972:0 (var)> +: ice-9/boot-9.scm:1685:16: In procedure raise-exception: +: Throw to key `bad!' with args `(#:972:0 (var)>)'. + +A solution may be to modify the my/sanitize procedure to accept something like + +#+BEGIN_SRC scheme +(my/sanitize var "" "'fieldname'" (list ((lambda (var) ...) . "list of unique numbers or strings"))) +#+END_SRC + +I have some example code [[file:opensmtpd-records.scm::;; TODO add in some code that accepts a (cons . cell) that of (proc . "error message") here.][here.]] It probably won't work, but it is a rough sketch +of what could work. +#+BEGIN_SRC scheme +[(eq? (cons? (car procedures))) +(cdr (car procedures))] +#+END_SRC + +Entering a new prompt. Type `,bt' for a backtrace or `,q' to continue. + +***** TODO Now make all the other sanitize sections that use a lambda use this new functionality: + +eg: + +#+BEGIN_SRC scheme +(family opensmtpd-listen-on-configuration-family + (default #f) + (sanitize (lambda (var) + (cond + [(eq? #f var) ;; var == #f + var] + [(and (string? var) + (or (string=? "inet4" var) + (string=? "inet6" var))) + var] + [else + (begin + (display " fieldname 'family' must be string \"inet4\" or \"inet6\".\n") + (throw 'bad! var))])))) +#+END_SRC + +***** TODO perhaps I can create an unexported record [0/2] + +fieldnames: 'procedure', 'error message'. +***** TODO Perhaps I could try to make my/sanitize work more like the opensmtpd-listen-on-configuration-filters does. + +It looks like tiny errors first. And shows you those relevent errors. When you +fix those tiny errors it starts looking for harder errors. + +This is nice because when you get something wrong in the config, you get the +specific error message. + +The way my/sanitize currently works, if you get one thing wrong, then you get 4 +reasons for what you might have done wrong. +***** TODO the has lost of hard coded error checking. +It would be nice to hook this up to my/sanitize. Along with other bits of the code. +**** TODO rework my/sanitize to be like opensmtpd-listen-on-configuration-filters + +opensmtpd-listen-on-configuration-filters works like so: + +Is the variable (not (false? var)) + +Is the variable (need-some-messages?) + +Does the variable (need-some-value) ? + +else var. + +For example, this +#+BEGIN_SRC scheme +(define-record-type* + opensmtpd-table-configuration make-opensmtpd-table-configuration + opensmtpd-table-configuration? + this-record + (name opensmtpd-table-configuration-name ;; string + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-table-configuration" "name" (list string?))))) + (file-db opensmtpd-table-configuration-file-db + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-table-configuration" "file-db" + (list boolean?))))) +#+END_SRC + +would become: +#+BEGIN_SRC scheme +(define-record-type* + opensmtpd-table-configuration make-opensmtpd-table-configuration + opensmtpd-table-configuration? + this-record + (name opensmtpd-table-configuration-name ;; string + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-table-configuration" "name" (list not-string?))))) + (file-db opensmtpd-table-configuration-file-db + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-table-configuration" "file-db" + (list not-boolean?))))) +#+END_SRC + +This +#+BEGIN_SRC scheme +(secure-connection opensmtpd-listen-on-configuration-secure-connection + (default #f) + (sanitize (lambda (var) + (cond [(boolean? var) + var] + [(and (string? var) + (string-in-list? var + (list "smtps" "tls" + "tls-require" + "tls-require-verify"))) + var] + [else + (begin + (display (string-append " fieldname 'secure-connection' can be " + "one of the following strings: \n'smtps', 'tls', 'tls-require', " + "or 'tls-require-verify'.\n")) + (throw 'bad! var))])))) +#+END_SRC + +would become (finish this thought exercise.) +#+BEGIN_SRC scheme +(secure-connection opensmtpd-listen-on-configuration-secure-connection + (default #f) + (sanitize (lambda (var) + (list not-boolean + (sanitize-proc-configuration + string-in-list? + (list "smtps" "tls" + "tls-require" + "tls-require-verify")) + [else + (begin + (display (string-append " fieldname 'secure-connection' can be " + "one of the following strings: \n'smtps', 'tls', 'tls-require', " + "or 'tls-require-verify'.\n")) + (throw 'bad! var))])))) +#+END_SRC + +**** PROJ can we merge my/sanitize syntax into (guix records)? + +#+BEGIN_SRC scheme +(define-record-type* + opensmtpd-option-configuration make-opensmtpd-option-configuration + opensmtpd-option-configuration? + (documentation (list " uses to\n" + "tweak various options.")) + (sanitize (sanitize-configuration ; this sanitizes the whole record. + (list (lambda (value) + ... + )))) + (option opensmtpd-option-configuration-option + (default #f) + (sanitize (sanitize-configuration + (list string?)))) + (not opensmtpd-option-configuration-not + (default #f) + (sanitize (sanitize-configuration + (list boolean?)) )) + (regex opensmtpd-option-configuration-regex + (default #f) + (sanitize (sanitize-configuration '(boolean?)))) + (value opensmtpd-option-configuration-value + (default #f) + (sanitize (sanitize-configuration + ;; note that it is smart enough to realize that opensmtpd-table-configuration? is a record, + ;; so the error message it returns is something like " fieldname is of + ;; type ." + (list false? string? opensmtpd-table-configuration?))))) +#+END_SRC +**** TODO maybe sanitize the ~~ fieldname 'matches' better + +#+BEGIN_SRC scheme +(opensmtpd-configuration (matches (list + (opensmtpd-match-configuration + (action + (opensmtpd-action-relay-configuration + (name "relay")))) + 345 + (opensmtpd-match-configuration + (for (opensmtpd-option-configuration + (option "for local"))) + (action + (opensmtpd-action-relay-configuration + (name "relay"))))))) +#+END_SRC + +#+BEGIN_EXAMPLE + fieldname: 'matches' is of type a list of unique records. + +ice-9/boot-9.scm:1685:16: In procedure raise-exception: +Throw to key `bad!' with args `((#< action: #< name: "relay" backup: #f backup-mx: #f helo: #f domain: #f host: #f pki: #f srs: #f tls: #f protocols: #f ciphers: #f auth: #f mail-from: #f src: #f> for: #f from: #f auth: #f helo: #f mail-from: #f rcpt-to: #f tag: #f tls: #f> 345 #< action: #< name: "relay" backup: #f backup-mx: #f helo: #f domain: #f host: #f pki: #f srs: #f tls: #f protocols: #f ciphers: #f auth: #f mail-from: #f src: #f> for: #< option: "for local" not: #f regex: #f value: #f> from: #f auth: #f helo: #f mail-from: #f rcpt-to: #f tag: #f tls: #f>))'. + +Entering a new prompt. Type `,bt' for a backtrace or `,q' to continue. +scheme@(opensmtpd-records) [24]> ,bt +In current input: + 2358:0 2 (_) +In /home/joshua/prog/gnu/guix/guix-config/linode-guix-system-configuration/opensmtpd-records.scm: + 278:8 1 (my/sanitize (#< action: #< name: "relay" backup: #f backup-mx: #f helo: #f domain…> …) …) +In ice-9/boot-9.scm: + 1685:16 0 (raise-exception _ #:continuable? _) +#+END_EXAMPLE + +It is not obvious from the error message what is wrong. The error message +should say + +#+BEGIN_SRC org +~= fieldname 'matches' is a list of unique =~. One of the items in the list +is '345'. +Throw to key `bad! with args 345 +#+END_SRC + +Alternatively, we could define a guix specific record printer to make it easier +to see the problem. + +#+BEGIN_SRC scheme + fieldname: 'matches' is of type a list of unique records. + +ice-9/boot-9.scm:1685:16: In procedure raise-exception: +Throw to key `bad!' with args: +`(list + (opensmtpd-match-configuration + (action + (opensmtpd-action-relay-configuration + (name "relay")))) + 345 + (opensmtpd-match-configuration + (for (opensmtpd-option-configuration + (option "for local"))) + (action + (opensmtpd-action-relay-configuration + (name "relay")))))'. + +Entering a new prompt. Type `,bt' for a backtrace or `,q' to continue. +scheme@(opensmtpd-records) [24]> ,bt +In current input: + 2358:0 2 (_) +In /home/joshua/prog/gnu/guix/guix-config/linode-guix-system-configuration/opensmtpd-records.scm: + 278:8 1 (my/sanitize (#< action: #< name: "relay" backup: #f backup-mx: #f helo: #f domain…> …) …) +In ice-9/boot-9.scm: + 1685:16 0 (raise-exception _ #:continuable? _) + +#+END_SRC +**** TODO some of the error messages say "bad var #f". This is not very helpful. + +Where it is useful I should do a ~(throw 'bad! record)~ instead of +~(throw `bad! #f)~ +*** PROJ add support for ~= fieldname 'senders': syntax "senders =~ [masquerade]" [0/4] +:LOGBOOK: +- State "TODO" from [2021-11-02 Tue 04:08] +:END: +**** TODO add a record type + +fieldnames: 'table (accepts '), and 'masquerade' (accepts boolean). +**** TODO change the sanitize portion of the fieldname 'senders' in the + +The below code does work in a REPL. +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +(opensmtpd-listen-on-configuration + (auth + (opensmtpd-table-configuration + (name "My-table") + (data '(("joshua" . "$some$Long$EncrytpedPassword")))))) +#+END_SRC + +#+RESULTS: + +AND the below code will correctly result in an error! +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +(opensmtpd-listen-on-configuration + (auth + (opensmtpd-table-configuration + (name "My-table") + (data '("joshua" "$some$Long$EncrytpedPassword"))))) +#+END_SRC + +#+RESULTS: +: ~= fieldname: 'auth' is of type boolean, or an =~ record whose fieldname 'values' are an assoc-list +: (eg: (opensmtpd-table-configuration (name "table") (data '("joshua" . "$encrypted$password")))). + +**** TODO change relevant portions in opensmtpd-listen-on-configuration->string +This bit of code works in the repl too! +#+BEGIN_SRC scheme :dir ~/prog/gnu/guix-config/linode-guix-system-configuration/ +(add-to-load-path (dirname (current-filename))) +(use-modules (opensmtpd-records)) + +((@@ (opensmtpd-records) opensmtpd-listen-on-configuration->string) + (opensmtpd-listen-on-configuration + (auth + (opensmtpd-table-configuration + (name "credentials") + (data '(("joshua" . "$someLongEncrytpedPassword"))))))) +#+END_SRC + +**** TODO support the masquerade option + +Right now, senders just accepts an , but I am not allowing the +user to turn on or off the masquerade option. +*** TODO write out of examples of ~~ records that will fail to start or do not make sense. +Provide appropriate error messages. + +There is a trend of guix services that "work" but are not dummy proof. For +example, the XMPP service wants a cert in the format of "~ record and just by looking at the +record tell, if you have done something silly that will make the service refuse +to start or behave in a weird way, and provide you appropriate error messages so +you don't have to go syntax hunting. + +Examples: + +- could be a filter that is defined but never used, which won't +be possible once [[id:89603b3f-7580-4531-8aee-2c115c97adfe][remove opensmtpd-configuration-filters]] is done. + +- (listen-on (interface "doesNotExist")) + +- (smtp-configuration (smtp-max-message-size "10G")) Are you sure you + want emails that large? + +- (pki (domain "name") (key "notAKeyfile.txt") (cert + "notACertFile.txt") + +- (ca (file "NotACaFile.txt")) + +- (opensmtpd-filter-phase-configuration (name "filter") (phase "helo") (decision "bypass")) + There is no fieldname =options= here. This has to be sanitized by + ~~'s fieldname 'filters'. + + +#+BEGIN_SRC scheme +(opensmtpd-listen-on-configuration + (filters + (list + (opensmtpd-filter-phase-configuration + (name "noFRDNS") + (phase "commit") + (options (list (opensmtpd-option-configuration + (option "fcrdns") + ))) + (decision "junk"))))) +#+END_SRC +* Some notes on working on the service workflows and such + +disabling centaur-tabs-mode seems to help. and NOT working in the console helps too. + +I think that having the geiser repl running via + +M-x geiser +M-x geiser-load-file RET opensmtpd-records.scm +,m (opensmtpd-records) + +May be causing Emacs to move slowly after a while. + + +It may be better to instead do: + +cd prog/gnu/guix-config/linode-system-configuration; +guile -L . --listen=9999 + +And then in Emacs (as described here: https://www.nongnu.org/geiser/geiser_3.html) +connect to the external repl via +M-x geiser-connect -- 2.36.1 From debbugs-submit-bounces@debbugs.gnu.org Mon Jul 04 17:18:27 2022 Received: (at 56046) by debbugs.gnu.org; 4 Jul 2022 21:18:27 +0000 Received: from localhost ([127.0.0.1]:48750 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o8TSc-0001xP-W1 for submit@debbugs.gnu.org; Mon, 04 Jul 2022 17:18:27 -0400 Received: from mx1.dismail.de ([78.46.223.134]:44270) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o8TSb-0001xD-DU for 56046@debbugs.gnu.org; Mon, 04 Jul 2022 17:18:26 -0400 Received: from mx1.dismail.de (localhost [127.0.0.1]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 76cbd521 for <56046@debbugs.gnu.org>; Mon, 4 Jul 2022 23:18:18 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; d=dismail.de; h=from:to:cc :subject:date:message-id:mime-version:content-type :content-transfer-encoding; s=20190914; bh=ceJBid/FRI4gvzBm8WZEC A4t0cZHcYhfqjxDCaFbeIs=; b=TLHey6ksSu89jO3AK7tXL4F2q2FYXhz/qKx+v eu9DlRCCoKyX8gFysMQG9fAtpFxh1dAzKAHrYiSsD7I1CilyVWLpD5lDPR15LgRW IkdeB7sohlDd2y3V4EuWTnt9j4nNxrPZ0Fk2AzUnGzjPkpEvCL6y3L31+mtAquVy o8dRdNdk37qUO/AYUS85z9Kj7x1mhnhbH4Wr2hMAUmbtb/8mp/PuzLfpgItNrmKl jcDMeETOzBPyCBTFc+I9KnKXEfhkd+n3ZYkZpsuE6Kxa1RQQ4Tm7Mg0iXRIaY1E1 rkPCVMjBQt8kBVgPU6aScuwu9i0tooKb8PP0sv6xo3ln2kCGQ== Received: from smtp2.dismail.de ( [10.240.26.12]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 5278a1d4 for <56046@debbugs.gnu.org>; Mon, 4 Jul 2022 23:18:17 +0200 (CEST) Received: from smtp2.dismail.de (localhost [127.0.0.1]) by smtp2.dismail.de (OpenSMTPD) with ESMTP id 6be3844b for <56046@debbugs.gnu.org>; Mon, 4 Jul 2022 23:18:17 +0200 (CEST) Received: by dismail.de (OpenSMTPD) with ESMTPSA id e8b786e8 (TLSv1.3:TLS_AES_256_GCM_SHA384:256:NO); Mon, 4 Jul 2022 23:18:09 +0200 (CEST) From: Joshua Branson To: 56046@debbugs.gnu.org Subject: [PATCH] services: mail: add opensmtpd records to enhance opensmtpd-configuration. Version 2 Date: Mon, 4 Jul 2022 17:17:59 -0400 Message-Id: <20220704211759.8314-1-jbranso@dismail.de> X-Mailer: git-send-email 2.36.1 MIME-Version: 1.0 Content-Type: text/plain; charset=y Content-Transfer-Encoding: 8bit X-Debbugs-Envelope-To: 56046 Cc: Joshua Branson X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" Openmstpd-configuration may only be configured by a config-file. This patch, enables one to configure opensmtpd by using some guile record types (defined via define-record-type*). * gnu/services/mail.scm: New records (opensmtpd-table-configuration), (opensmtpd-ca-configuration), (opensmtpd-pki-configuration), (opensmtpd-action-local-delivery-configuration), (opensmtpd-maildir-configuration), (opensmtpd-mda-configuration), (opensmtpd-action-relay-configuration), (opensmtpd-option-configuration), (opensmtpd-filter-phase-configuration), (opensmtpd-filter-configuration), (opensmtpd-listen-on-configuration), (opensmtpd-listen-on-socket-configuration), (opensmtpd-match-configuration), (opensmtpd-smtp-configuration), (opensmtpd-srs-configuration), (opensmtpd-queue-configuration), and (opensmtpd-configuration). New procedures: false?, is-value-right-type, add-comma-or-string, file-exists?, list-of-procedures->string, string-in-list?, my-sanitize, opensmtpd-filter-chain?, throw-error-duplicate-option, sanitize-list-of-options-for-match-configuration, sanitize-filters, list-has-duplicates-or-non-filters?, filter-phase-has-message-and-value?, filter-phase-decision-lacks-proper-message?, filter-phase-lacks-proper-value?, filter-phase-has-incorrect-junk-or-bypass?, filter-phase-junks-after-commit?, list-of-unique-filter-or-filter-phase?, throw-error, contains-duplicate?, list-of-type?, list-of-strings?, list-of-unique-opensmtpd-option-configuration?, list-of-opensmtpd-ca-configuration?, list-of-opensmtpd-pki-configuration?, list-of-opensmtpd-listen-on-configuration?, list-of-unique-opensmtpd-match-configuration?, list-of-strings->string, assoc-list? assoc-list, variable->string, table-whose-data-are-assoc-list?, table-whose-data-are-a-list-of-strings?, assoc-list->string, opensmtpd-table-configuration->string, opensmtpd-listen-on-configuration->string, opensmtpd-listen-on-socket-configuration->string, opensmtpd-action-relay-configuration->string, opensmtpd-lmtp-configuration->string, opensmtpd-mda-configuration->string, opensmtpd-maildir-configuration->string, opensmtpd-action-local-delivery-configuration->string, opensmtpd-action->string, opensmtpd-option-configuration->string, opensmtpd-match-configuration->string, opensmtpd-ca-configuration->string, opensmtpd-pki-configuration->string, generate-filter-chain-name, opensmtpd-filter-chain->string, opensmtpd-filter-phase-configuration->string, opensmtpd-filters->string, opensmtpd-configuration-listen->string, opensmtpd-configuration-srs->string, opensmtpd-smtp-configuration->string, opensmtpd-configuration-queue->string, get-opensmtpd-actions, get-opensmtpd-pki-configurations, get-opensmtpd-filters, flatten, get-opensmtpd-tables, opensmtpd-configuration-fieldname->string, list-of-records->string, opensmtpd-configuration->mixed-text-file. * doc/guix.texi added documentation for the new records for opensmtpd. --- doc/guix.texi | 1051 ++++++++++++++++++++- gnu/services/mail.scm | 2016 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 3056 insertions(+), 11 deletions(-) diff --git a/doc/guix.texi b/doc/guix.texi index eda0956260..e8564240d1 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -24849,14 +24849,59 @@ could instantiate a dovecot service like this: @subsubheading OpenSMTPD Service @deffn {Scheme Variable} opensmtpd-service-type -This is the type of the @uref{https://www.opensmtpd.org, OpenSMTPD} -service, whose value should be an @code{opensmtpd-configuration} object -as in this example: - -@lisp -(service opensmtpd-service-type - (opensmtpd-configuration - (config-file (local-file "./my-smtpd.conf")))) +OpenSMTPD is an easy-to-use mail transfer agent (MTA). Its configuration file is +throughly documented in @code{man 5 smtpd.conf}. OpenSMTPD @strong{listens} for incoming +mail and @strong{matches} the mail to @strong{actions}. The following records represent those +stages: + +@multitable {aaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @strong{listens} +@tab @code{} +@item +@tab +@item @strong{matches} +@tab @code{} +@item +@tab +@item @strong{actions} +@tab @code{} +@item +@tab @code{} +@end multitable + +Additionally, each @code{} and +@code{} may use a list of +@code{}, and/or +@code{} records to filter email/spam. Also +numerous records' fieldnames use @code{} to hold lists +or key value pairs of data. + +A simple example configuration is below: + +@lisp +(let ((smtp.gnu.org (opensmtpd-pki-configuration + (domain "smtp.gnu.org") + (cert "file.cert") + (key "file.key")))) + (service opensmtpd-service-type + (opensmtpd-configuration + (listen-ons (list + (opensmtpd-listen-on-configuration + (pki smtp.gnu.org)) + (opensmtpd-listen-on-configuration + (pki smtp.gnu.org) + (secure-connection "smtps")))) + (matches (list + (opensmtpd-match-configuration + (action + (opensmtpd-action-local-delivery-configuration + (name "local-delivery")))) + (opensmtpd-match-configuration + (action + (opensmtpd-action-relay-configuration + (name "relay"))))))))) @end lisp @end deffn @@ -24873,9 +24918,999 @@ it listens on the loopback network interface, and allows for mail from users and daemons on the local machine, as well as permitting email to remote servers. Run @command{man smtpd.conf} for more information. +@item @code{bounce} (default: @code{(list "4h")}) + +@code{bounce} is a list of strings, which send warning messages to the envelope +sender when temporary delivery failures cause a message to remain in the +queue for longer than string delay. Each string delay parameter consists +of a string beginning with a positive decimal integer and a unit 's', 'm', 'h', +or 'd'. At most four delay parameters can be specified. + +@item @code{listen-ons} (default: @code{(list (opensmtpd-listen-on-configuration))}) + +@code{listen-ons} is a list of @code{} records. +This list details what interfaces and ports OpenSMTPD listens on as well as +other information. + +@item @code{listen-on-socket} (default: @code{(opensmtpd-listen-on-socket-configuration-configuration)}) + +Listens for incoming connections on the Unix domain socket. + +@item @code{includes} (default: @code{#f}) + +@code{includes} is a list of string filenames. Each filename's contents is +additional configuration that is inserted into the top of the configuration +file. + +@item @code{matches} default: + +@lisp + (list (opensmtpd-match-configuration + (action (opensmtpd-action-local-delivery-configuration + (name "local") + (method "mbox"))) + (for (opensmtpd-option-configuration + (option "for local")))) + (opensmtpd-match-configuration + (action (opensmtpd-action-relay-configuration + (name "outbound"))) + (from (opensmtpd-option-configuration + (option "from local"))) + (for (opensmtpd-option-configuration + (option "for any"))))) +@end lisp + +@code{matches} is a list of @code{} records, which +matches incoming mail and sends it to a correspending action. The match +records are evaluated sequentially, with the first match winning. If an +incoming mail does not match any match records, then it is rejected. +@c put this backin? @end itemize + +@c put this back in? @itemize +@item @code{mta-max-deferred} (default: @code{100}) + +When delivery to a given host is suspended due to temporary failures, cache +at most number envelopes for that host such that they can be delivered as +soon as another delivery succeeds to that host. The default is 100. + +@item @code{queue} (default: @code{#f}) + +@code{queue} expects an @code{} record. With it, one may +compress and encrypt queue-ed emails as well as set the default expiration +time for temporarily undeliverable messages. + +@item @code{smtp} (default: @code{#f}) + +@code{smtp} expects an @code{} record, which lets one +specifiy how large email may be along with other settings. + +@item @code{srs} (default: @code{#f}) + +@code{srs} expects an @code{} record, which lets one set +up SRS, the Sender Rewritting Scheme. @end table @end deftp +@itemize +@item +Data Type: opensmtpd-listen-on-configuration + +Data type representing the configuration of an +@code{}. Listen on the fieldname @code{interface} for +incoming connections, using the same syntax as for ifconfig(8). The interface +parameter may also be an string interface group, an string IP address, or a +string domain name. Listening can optionally be restricted to a specific +address fieldname @code{family}, which can be either ``inet4'' or ``inet6''. + +@itemize +@item @code{interface} (default: ``lo'') + +The string interface to listen for incoming connections. These interface can +usually be found by the command @code{ip link}. + +@item @code{family} (default: @code{#f}) + +The string IP family to use. Valid strings are ``inet4'' or ``inet6''. + +@item @code{auth} (default: @code{#f}) + +Support SMTPAUTH: clients may only start SMTP transactions after successful +authentication. If @code{auth} is @code{#t}, then users are authenticated against +their own normal login credentials. Alternatively @code{auth} may be an +@code{} whose users are authenticated against +their passwords. + +@item @code{auth-optional} (default: @code{#f}) + +Support SMTPAUTH optionally: clients need not authenticate, but may do so. +This allows the @code{} to both accept +incoming mail from untrusted senders and permit outgoing mail from +authenticated users (using @code{} fieldname +@code{auth}). It can be used in situations where it is not possible to listen on +a separate port (usually the submission port, 587) for users to +authenticate. + +@item @code{filters} (default: @code{#f}) + +A list of one or many @code{} or +@code{} records. The filters are applied +sequentially. These records listen and filter on connections handled by this +listener. + +@item @code{hostname} (default: @code{#f}) + +Use string ``hostname'' in the greeting banner instead of the default server +name. + +@item @code{hostnames} (default: @code{#f}) + +Override the server name for specific addresses. Use a +@code{} containing a mapping of string IP +addresses to hostnames. If the address on which the connection arrives +appears in the mapping, the associated hostname is used. + +@item @code{mask-src} (default: @code{#f}) + +If @code{#t}, then omit the from part when prepending “Received” headers. + +@item @code{disable-dsn} (default: @code{#f}) + +When @code{#t}, then disable the DSN (Delivery Status Notification) extension. + +@item @code{pki} (default: @code{#f}) + +For secure connections, use an @code{} +to prove a mail server's identity. + +@item @code{port} (default: @code{#f}) + +Listen on the integer port instead of the default port of 25. + +@item @code{proxy-v2} (default: @code{#f}) + +If @code{#t}, then support the PROXYv2 protocol, rewriting appropriately source +address received from proxy. + +@item @code{received-auth} (default: @code{#f}) + +If @code{#t}, then in “Received” headers, report whether the session was +authenticated and by which local user. + +@item @code{senders} (default: @code{#f}) + +Look up the authenticated user in the supplied +@code{} to find the email addresses that user is +allowed to submit mail as. + +@item @code{secure-connection} (default: @code{#f}) + +This is a string of one of these options: + +@multitable {aaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item ``smtps'' +@tab Support SMTPS, by default on port 465. +@item ``tls'' +@tab Support STARTTLS, by default on port 25. +@item ``tls-require-verify'' +@tab Like tls, but force clients to establish +@item +@tab a secure connection before being allowed to +@item +@tab start an SMTP transaction. With the verify +@item +@tab option, clients must also provide a valid +@item +@tab certificate to establish an SMTP session. +@end multitable + +@item @code{tag} (default: @code{#f}) + +Clients connecting to the listener are tagged with the given string tag. +@end itemize + +@item Data Type: opensmtpd-listen-on-socket-configuration + +Data type representing the configuration of an +@code{}. Listen for incoming SMTP +connections on the Unix domain socket @samp{/var/run/smtpd.sock}. This is done by +default, even if the directive is absent. + +@itemize +@item @code{filters} (default: @code{#f}) + +A list of one or many @code{} or +@code{} records. These filter incoming +connections handled by this listener. + +@item @code{mask-src} (default: @code{#f}) + +If @code{#t}, then omit the from part when prepending “Received” headers. + +@item @code{tag} (default: @code{#f}) + +Clients connecting to the listener are tagged with the given string tag. +@end itemize + +@item Data Type: opensmtpd-match-configuration + +This data type represents the configuration of an +@code{} record. + +If at least one mail envelope matches the options of one match record, receive +the incoming message, put a copy into each matching envelope, and atomically +save the envelopes to the mail spool for later processing by the respective +@code{} found in fieldname @code{action}. + +@itemize +@item @code{action} (default: @code{#f}) + +If mail matches this match configuration, then do this action. Valid values +include @code{} or +@code{}. + +@item @code{options} (default: @code{#f}) @code{} +The fieldname 'option' is a list of unique +@code{} records. + +Each @code{} record's fieldname 'option' has some +mutually exclusive options: there can be only one ``for'' and only one ``from'' option. + +@multitable {aaaaaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@headitem for +@tab from +@item only use one of the following: +@tab only use one of the following: +@item ``for any'' +@tab ``from any'' +@item ``for local'' +@tab ``from auth'' +@item ``for domain'' +@tab ``from local'' +@item ``for rcpt-to'' +@tab ``from mail-from'' +@item +@tab ``from socket'' +@item +@tab ``from src'' +@end multitable + +The following matching options are supported and can all be negated (via not +#t). The options that support a table (anything surrounded with '<' and '>' +eg:
), also support specifying regex via (regex #t). + +@itemize +@item @samp{for any} + +Specify that session may address any destination. + +@item @samp{for local} + +Specify that session may address any local domain. This is the default, +and may be omitted. + +@item @samp{for domain _domain_ | } + +Specify that session may address the string or list table domain. + +@item @samp{for rcpt-to _recipient_ | } + +Specify that session may address the string or list table recipient. + +@item @samp{from any} + +Specify that session may originate from any source. + +@item @samp{from auth} + +Specify that session may originate from any authenticated user, no matter +the source IP address. + +@item @samp{from auth _user_ | } + +Specify that session may originate from authenticated user or user list +user, no matter the source IP address. + +@item @samp{from local} + +Specify that session may only originate from a local IP address, or from +the local enqueuer. This is the default, and may be omitted. + +@item @samp{from mail-from _sender_ | } + +Specify that session may originate from sender or table sender, no +matter the source IP address. + +@item @samp{from rdns} + +Specify that session may only originate from an IP address that resolves +to a reverse DNS@. + +@item @samp{from rdns _hostname_ | } + +Specify that session may only originate from an IP address that resolves +to a reverse DNS matching string or list string hostname. + +@item @samp{from socket} + +Specify that session may only originate from the local enqueuer. + +@item @samp{from src _address_ |
} + +Specify that session may only originate from string or list table address +which can be a specific address or a subnet expressed in CIDR-notation. + +@item @samp{auth} + +Matches transactions which have been authenticated. + +@item @samp{auth _username_ | } + +Matches transactions which have been authenticated for user or user list +username. + +@item @samp{helo _helo-name_ | } + +Specify that session's HELO / EHLO should match the string or list table +helo-name. + +@item @samp{mail-from _sender_ | } + +Specify that transactions's MAIL FROM should match the string or list +table sender. + +@item @samp{rcpt-to _recipient_ | } + +Specify that transaction's RCPT TO should match the string or list table +recipient. + +@item @samp{tag tag} +Matches transactions tagged with the given tag. + +@item @samp{tls} +Specify that transaction should take place in a TLS channel. +@end itemize + +Here is a simple example: +@lisp + (opensmtpd-option-configuration + (not #t) + (regex #f) + (option "for domain") + (data (opensmtpd-table-configuration + (name "domain-table") + (data (list "gnu.org" "dismail.de"))))) +@end lisp + +The mail must NOT come from the domains @samp{gnu.org} or @samp{dismail.de}. + +@item Data Type: opensmtpd-option-configuration +@end itemize + +@item Data Type: opensmtpd-action-local-delivery-configuration + +This data type represents the configuration of an +@code{} record. + +@itemize +@item +@code{name} (default: @code{#f}) + +@code{name} is the string name of the relay action. + +@item @code{method} (default: @code{"mbox"}) + +The email delivery option. Valid options are: + +@itemize +@item @code{"mbox"} + +Deliver the message to the user's mbox with mail.local(8). + +@item @code{"expand-only"} + +Only accept the message if a delivery method was specified in an aliases +or .forward file. + +@item @code{"forward-only"} + +Only accept the message if the recipient results in a remote address after +the processing of aliases or forward file. + +@item @code{} + +Deliver the message to an LMTP server at +@code{}'s fieldname @code{destination}. The location +may be expressed as string host:port or as a UNIX socket. Optionally, +@code{}'s fieldname @code{rcpt-to} might be specified +to use the recipient email address (after expansion) instead of the local +user in the LMTP session as RCPT TO@. + +@item @code{} + +Deliver the message to the maildir in +@code{}'s fieldname @code{pathname} if specified, +or by default to @samp{~/Maildir}. + +The pathname may contain format specifiers that are expanded before use +(see the below section about Format Specifiers). + +If @code{}'s record fieldname @code{junk} is @code{#t}, +then message will be moved to the ‘Junk’ folder if it contains a positive +‘X-Spam’ header. This folder will be created under fieldname @code{pathname} if +it does not yet exist. + +@item @code{} + +Delegate the delivery to the @code{}'s fieldname +@code{command} (type string) that receives the message on its standard input. + +The @code{command} may contain format specifiers that are expanded before use +(see Format Specifiers). +@end itemize + +@item @code{alias} (default: @code{#f}) + +Use the mapping table for aliases expansion. @code{alias} is an +@code{}. + +@item @code{ttl} (default: @code{#f}) + +@code{ttl} is a string specify how long a message may remain in the queue. It's +format is @samp{n@{s|m|h|d@}}. eg: ``4m'' is four minutes. + +@item @code{user} (default: @code{#f} ) + +@code{user} is the string username for performing the delivery, to be looked up +with getpwnam(3). + +This is used for virtual hosting where a single username is in charge of +handling delivery for all virtual users. + +This option is not usable with the mbox delivery method. + +@item @code{userbase} (default: @code{#f}) + +@code{userbase} is an @code{} record for mapping user +lookups instead of the getpwnam(3) function. + +The fieldnames @code{user} and @code{userbase} are mutually exclusive. + +@item @code{virtual} (default: @code{#f}) + +@code{virtual} is an @code{} record is used for virtual +expansion. +@end itemize + +@item Data Type: opensmtpd-action-relay-configuration + +This data type represents the configuration of an +@code{} record. + +@itemize +@item @code{name} (default: @code{#f}) + +@code{name} is the string name of the relay action. + +@item @code{backup} (default: @code{#f}) + +When @code{#t}, operate as a backup mail exchanger delivering messages to any +mail exchanger with higher priority. + +@item @code{backup-mx} (default: @code{#f}) + +Operate as a backup mail exchanger delivering messages to any mail exchanger +with higher priority than mail exchanger identified as string name. + +@item @code{helo} (default: @code{#f}) + +Advertise string heloname as the hostname to other mail exchangers during +the HELO phase. + +@item @code{helo-src} (default: @code{#f} ) + + Use the mapping @code{} to look up a hostname +matching the source address, to advertise during the HELO phase. + +@item @code{domain} (default: @code{#f}) + +Do not perform MX lookups but look up destination domain in an +@code{} and use matching relay url as relay host. + +@item @code{host} (default: @code{#f}) + +Do not perform MX lookups but relay messages to the relay host described by +the string relay-url. The format for relay-url is +@samp{[proto://[label@@]]host[:port]}. The following protocols are available: + +@multitable {aaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item smtp +@tab Normal SMTP session with opportunistic STARTTLS (the default). +@item smtp+tls +@tab Normal SMTP session with mandatory STARTTLS@. +@item smtp+notls +@tab Plain text SMTP session without TLS@. +@item lmtp +@tab LMTP session. port is required. +@item smtps +@tab SMTP session with forced TLS on connection, default port is +@item +@tab 465. +@end multitable + +Unless noted, port defaults to 25. + +The label corresponds to an entry in a credentials table, as documented in +@samp{table(5)}. It is used with the @samp{"smtp+tls"} and @samp{"smtps"} protocols for +authentication. Server certificates for those protocols are verified by +default. + +@item @code{pki} (default: @code{#f}) + +For secure connections, use the certificate associated with +@code{} (declared in a pki directive) to prove the +client's identity to the remote mail server. + +@item @code{srs} (default: @code{#f}) + +If @code{#t}, then when relaying a mail resulting from a forward, use the Sender +Rewriting Scheme to rewrite sender address. + +@item @code{tls} (default: @code{#f}) boolean or string ``no-verify'' + +When @code{#t}, Require TLS to be used when relaying, using mandatory STARTTLS by +default. When used with a smarthost, the protocol must not be +@samp{"smtp+notls://"}. When string @code{"no-verify"}, then do not require a valid +certificate. + +@item @code{auth} (default: @code{#f}) @code{} + +Use the alist @code{} for connecting to relay-url +using credentials. This option is usable only with fieldname @code{host} option. + +@item @code{mail-from} (default: @code{#f}) string + +Use the string mailaddress as MAIL FROM address within the SMTP transaction. + +@item @code{src} (default: @code{#f}) string | @code{} + +Use the string or @code{} sourceaddr for the +source IP address, which is useful on machines with multiple interfaces. If +the list contains more than one address, all of them are used in such a way +that traffic is routed as efficiently as possible. +@end itemize + +@item Data Type: opensmtpd-filter-configuration + +This data type represents the configuration of an +@code{}. This is the filter record one should use +if they want to use an external package to filter email eg: rspamd or +spamassassin. + +@itemize +@item @code{name} (default: @code{#f}) + +The string name of the filter. + +@item @code{proc} (default: @code{#f}) + +The string command or process name. If @code{proc-exec} is @code{#t}, @code{proc} is +treated as a command to execute. Otherwise, it is a process name. + +@item @code{proc-exec} (default: @code{#f}) +@end itemize + +@item Data Type: opensmtpd-filter-phase-configuration + +This data type represents the configuration of an +@code{}. + +In a regular workflow, smtpd(8) may accept or reject a message based only on +the content of envelopes. Its decisions are about the handling of the message, +not about the handling of an active session. + +Filtering extends the decision making process by allowing smtpd(8) to stop at +each phase of an SMTP session, check that options are met, then decide if a +session is allowed to move forward. + +With filtering via an @code{} record, a +session may be interrupted at any phase before an envelope is complete. A +message may also be rejected after being submitted, regardless of whether the +envelope was accepted or not. + +@itemize +@item @code{name} (default: @code{#f}) + +The string name of the filter phase. + +@item @code{phase-name} (default: @code{#f}) + +The string name of the phase. Valid values are: + +@multitable {aaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item ``connect'' +@tab upon connection, before a banner is displayed +@item ``helo'' +@tab after HELO command is submitted +@item ``ehlo'' +@tab after EHLO command is submitted +@item ``mail-from'' +@tab after MAIL FROM command is submitted +@item ``rcpt-to'' +@tab after RCPT TO command is submitted +@item ``data'' +@tab after DATA command is submitted +@item ``commit'' +@tab after message is fully is submitted +@end multitable + +@item @code{options} (default @code{#f}) + +A list of unique @code{} records. + +At each phase, various options, specified by a list of +@code{}, may be checked. The +@code{}'s fieldname 'option' values of: ``fcrdns'', +``rdns'', and ``src'' data are available in all phases, but other data must have +been already submitted before they are available. Options with a @samp{
} +next to them require the @code{}'s fieldname +@code{data} to be an @code{}. There are the available +options: + +@multitable {aaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item fcrdns +@tab forward-confirmed reverse DNS is valid +@item rdns +@tab session has a reverse DNS +@item rdns
+@tab session has a reverse DNS in table +@item src
+@tab source address is in table +@item helo
+@tab helo name is in table +@item auth +@tab session is authenticated +@item auth
+@tab session username is in table +@item mail-from
+@tab sender address is in table +@item rcpt-to
+@tab recipient address is in table +@end multitable + +These conditions may all be negated by setting +@code{}'s fieldname @code{not} to @code{#t}. + +Any conditions that require a table may indicate that tables include regexs +setting @code{}'s fieldname @code{regex} to @code{#t}. + +@item @code{decision} + +A string decision to be taken. Some decisions require an @code{message} or +@code{value}. Valid strings are: + +@multitable {aaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item ``bypass'' +@tab the session or transaction bypasses filters +@item ``disconnect'' message +@tab the session is disconnected with message +@item ``junk'' +@tab the session or transaction is junked, i.e., an +@item +@tab ‘X-Spam: yes’ header is added to any messages +@item ``reject'' message +@tab the command is rejected with message +@item ``rewrite'' value +@tab the command parameter is rewritten with value +@end multitable + +Decisions that involve a message require that the message be RFC valid, +meaning that they should either start with a 4xx or 5xx status code. +Descisions can be taken at any phase, though junking can only happen before +a message is committed. + +@item @code{message} (default @code{#f}) + +A string message beginning with a 4xx or 5xx status code. + +@item @code{value} (default: @code{#f}) + +A number value. @code{value} and @code{message} are mutually exclusive. +@end itemize + +@item Data Type: opensmtpd-option-configuration + +This data type represents the configuration of an +@code{}, which is used by +@code{} and @code{} +to match various options for email. + +@itemize +@item @code{conditition} (default @code{#f}) + +A string option to be taken. Some options require a string or an +@code{} via the fieldname data. When the option +record is used inside of an @code{}, then +valid strings are: + +At each phase, various options may be matched. The fcrdns, rdns, and src +data are available in all phases, but other data must have been already +submitted before they are available. + +@multitable {aaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item ``fcrdns'' +@tab forward-confirmed reverse DNS is valid +@item ``rdns'' +@tab session has a reverse DNS +@item ``rdns''
+@tab session has a reverse DNS in table +@item ``src''
+@tab source address is in table +@item ``helo''
+@tab helo name is in table +@item ``auth'' +@tab session is authenticated +@item ``auth''
+@tab session username is in table +@item ``mail-from''
+@tab sender address is in table +@item ``rcpt-to''
+@tab recipient address is in table +@end multitable + +When @code{} is used inside of an +@code{}, then valid strigs for fieldname @code{option} +are: ``for'', ``for any'', ``for local'', ``for domain'', ``for rcpt-to'', ``from any'' +``from auth'', ``from local'', ``from mail-from'', ``from rdns'', ``from socket'', +``from src'', ``auth'', ``helo'', ``mail-from'', ``rcpt-to'', ``tag'', or ``tls''. + +@item @code{data} (default @code{#f}) @code{} + +Some options require a table to be present. One would specify that table +here. +@item @code{regex} (default: @code{#f}) boolean + +Any options using a table may indicate that tables hold regex by +prefixing the table name with the keyword regex. + +@item @code{not} (default: @code{#f}) boolean + +When @code{#t}, this option record is negated. +@end itemize + +@item Data Type: opensmtpd-table-configuration + +This data type represents the configuration of an +@code{}. + +@itemize +@item @code{name} (default @code{#f}) + +@code{name} is the name of the @code{} record. + +@item @code{data} (default: @code{#f}) + +@code{data} expects a list of strings or an alist, which is a list of +cons cells. eg: @code{(data (list ("james" . "password")))} OR +@code{(data (list ("gnu.org" "fsf.org")))}. +@end itemize + +@item Data Type: opensmtpd-pki-configuration + +This data type represents the configuration of an +@code{}. + +@itemize +@item @code{domain} (default @code{#f}) + +@code{domain} is the string name of the @code{} record. + +@item @code{cert} (default: @code{#f}) + +@code{cert} (default: @code{#f}) + +@code{cert} is the string certificate filename to use for this pki. + +@item @code{key} (default: @code{#f}) + +@code{key} is the string certificate falename to use for this pki. + +@item @code{dhe} (default: @code{"none"}) + +Specify the DHE string parameter to use for DHE cipher suites with host +pkiname. Valid parameter values are ``none'', ``legacy'', or ``auto''. For ``legacy'', a +fixed key length of 1024 bits is used, whereas for ``auto'', the key length is +determined automatically. The default is ``none'', which disables DHE cipher +suites. +@end itemize + +@item Data Type: opensmtpd-maildir-configuration + +@itemize +@item @code{pathname} (default: @code{"~/Maildir"}) + +Deliver the message to the maildir if pathname if specified, or by default +to @samp{~/Maildir}. + +The pathname may contain format specifiers that are expanded before use +(see FORMAT SPECIFIERS). + +@item @code{junk} (default: @code{#f}) + +If the junk argument is @code{#t}, then the message will be moved to the @samp{‘Junk’} +folder if it contains a positive @samp{‘X-Spam’} header. This folder will be +created under pathname if it does not yet exist. +@end itemize + +@item Data Type: opensmtpd-mda-configuration + +@itemize +@item @code{name} + +The string name for this MDA command. + +@item @code{command} + +Delegate the delivery to a command that receives the message on its standard +input. + +The command may contain format specifiers that are expanded before use (see +FORMAT SPECIFIERS). +@end itemize + +@item Data Type: opensmtpd-queue-configuration + +@itemize +@item @code{compression} (default @code{#f}) + +Store queue files in a compressed format. This may be useful to save disk +space. + +@item @code{encryption} (default @code{#f}) + +Encrypt queue files with EVP@math{_aes}@math{_256}@math{_gcm}(3). If no key is specified, it is +read with getpass(3). If the string stdin or a single dash (‘-’) is given +instead of a key, the key is read from the standard input. + +@item @code{ttl-delay} (default @code{#f}) + +Set the default expiration time for temporarily undeliverable messages, +given as a positive decimal integer followed by a unit s, m, h, or d. The +default is four days (``4d''). +@end itemize + +@item Data Type: opensmtpd-smtp-configuration + +Data type representing an @code{} record. + +@itemize +@item @code{ciphers} (default: @code{#f}) + +Set the control string for SSL@math{_CTX}@math{_set}@math{_cipher}@math{_list}(3). The default is + ``HIGH:!aNULL:!MD5''. + +@item @code{limit-max-mails} (default: @code{100}) + +Limit the number of messages to count for each sessio + +@item @code{limit-max-rcpt} (default: @code{1000}) + +Limit the number of recipients to count for each transaction. + +@item @code{max-message-size} (default: @code{35M}) + +Reject messages larger than size, given as a positive number of bytes or as +a string to be parsed with scan@math{_scaled}(3). + +@item @code{sub-addr-delim character} (default: @code{+}) + +When resolving the local part of a local email address, ignore the ASCII +character and all characters following it. This is helpful for email +filters. @samp{"admin+bills@@gnu.org"} is the same email address as +@samp{"admin@@gnu.org"}. BUT an email filter can filter emails addressed to first +email address into a 'Bills' email folder. +@end itemize + +@item Data Type: opensmtpd-srs-configuration + +@itemize +@item @code{key} (default: @code{#f}) + +Set the secret key to use for SRS, the Sender Rewriting Scheme. + +@item @code{backup-key} (default: @code{#f}) + +Set a backup secret key to use as a fallback for SRS@. This can be used to +implement SRS key rotation. + +@item @code{ttl-delay} (default: @code{"4d"}) + +Set the time-to-live delay for SRS envelopes. After this delay, a bounce +reply to the SRS address will be discarded to limit risks of forged +addresses. +@end itemize + +@item Format Specifiers + +Some configuration records support expansion of their parameters at +runtime. Such records (for example +@code{}, @code{}) may use +format specifiers which are expanded before delivery or relaying. The +following formats are currently supported: + +@multitable {aaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @samp{%@{sender@}} +@tab sender email address, may be empty string +@item @samp{%@{sender.user@}} +@tab user part of the sender email address, may be empty +@item @samp{%@{sender.domain@}} +@tab domain part of the sender email address, may be empty +@item @samp{%@{rcpt@}} +@tab recipient email address +@item @samp{%@{rcpt.user@}} +@tab user part of the recipient email address +@item @samp{%@{rcpt.domain@}} +@tab domain part of the recipient email address +@item @samp{%@{dest@}} +@tab recipient email address after expansion +@item @samp{%@{dest.user@}} +@tab user part after expansion +@item @samp{%@{dest.domain@}} +@tab domain part after expansion +@item @samp{%@{user.username@}} +@tab local user +@item @samp{%@{user.directory@}} +@tab home directory of the local user +@item @samp{%@{mbox.from@}} +@tab name used in mbox From separator lines +@item @samp{%@{mda@}} +@tab mda command, only available for mda wrappers +@end multitable + +Expansion formats also support partial expansion using the optional bracket notations +with substring offset. For example, with recipient domain @samp{“example.org”}: + +@multitable {aaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaa} +@item @samp{%@{rcpt.domain[0]@}} +@tab expands to “e” +@item @samp{%@{rcpt.domain[1]@}} +@tab expands to “x” +@item @samp{%@{rcpt.domain[8:]@}} +@tab expands to “org” +@item @samp{%@{rcpt.domain[-3:]@}} +@tab expands to “org” +@item @samp{%@{rcpt.domain[0:6]@}} +@tab expands to “example” +@item @samp{%@{rcpt.domain[0:-4]@}} +@tab expands to “example” +@end multitable + +In addition, modifiers may be applied to the token. For example, with recipient +@samp{“User+Tag@@Example.org”}: + +@multitable {aaaaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @samp{%@{rcpt:lowercase@}} +@tab expands to “user+tag@@example.org” +@item @samp{%@{rcpt:uppercase@}} +@tab expands to “USER+TAG@@EXAMPLE.ORG” +@item @samp{%@{rcpt:strip@}} +@tab expands to “User@@Example.org” +@item @samp{%@{rcpt:lowercasestrip@}} +@tab expands to “user@@example.org” +@end multitable + +For security concerns, expanded values are sanitized and potentially dangerous +characters are replaced with ‘:’. In situations where they are desirable, the +“raw” modifier may be applied. For example, with recipient +@samp{“user+t?g@@example.org”}: + +@multitable {aaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @samp{%@{rcpt@}} +@tab expands to “user+t:g@@example.org” +@item @samp{%@{rcpt:raw@}} +@tab expands to “user+t?g@@example.org” +@end multitable +@end itemize + @subsubheading Exim Service @cindex mail transfer agent (MTA) diff --git a/gnu/services/mail.scm b/gnu/services/mail.scm index d99743ac31..2a344e303e 100644 --- a/gnu/services/mail.scm +++ b/gnu/services/mail.scm @@ -57,8 +57,143 @@ (define-module (gnu services mail) mailbox-configuration namespace-configuration + opensmtpd-table-configuration + opensmtpd-table-configuration? + opensmtpd-table-configuration-name + opensmtpd-table-configuration-file-db + opensmtpd-table-configuration-data + + opensmtpd-ca-configuration + opensmtpd-ca-configuration? + opensmtpd-ca-configuration-name + opensmtpd-ca-configuration-file + + opensmtpd-pki-configuration + opensmtpd-pki-configuration? + opensmtpd-pki-configuration-domain + opensmtpd-pki-configuration-cert + opensmtpd-pki-configuration-key + opensmtpd-pki-configuration-dhe + + opensmtpd-action-local-delivery-configuration + opensmtpd-action-local-delivery-configuration? + opensmtpd-action-local-delivery-configuration-method + opensmtpd-action-local-delivery-configuration-alias + opensmtpd-action-local-delivery-configuration-ttl + opensmtpd-action-local-delivery-configuration-user + opensmtpd-action-local-delivery-configuration-userbase + opensmtpd-action-local-delivery-configuration-virtual + opensmtpd-action-local-delivery-configuration-wrapper + + opensmtpd-maildir-configuration + opensmtpd-maildir-configuration? + opensmtpd-maildir-configuration-pathname + opensmtpd-maildir-configuration-junk + + opensmtpd-mda-configuration + opensmtpd-mda-configuration-name + opensmtpd-mda-configuration-command + + opensmtpd-action-relay-configuration + opensmtpd-action-relay-configuration? + opensmtpd-action-relay-configuration-backup + opensmtpd-action-relay-configuration-backup-mx + opensmtpd-action-relay-configuration-helo + opensmtpd-action-relay-configuration-domain + opensmtpd-action-relay-configuration-host + opensmtpd-action-relay-configuration-pki + opensmtpd-action-relay-configuration-srs + opensmtpd-action-relay-configuration-tls + opensmtpd-action-relay-configuration-auth + opensmtpd-action-relay-configuration-mail-from + opensmtpd-action-relay-configuration-src + + opensmtpd-option-configuration + opensmtpd-option-configuration? + opensmtpd-option-configuration-option + opensmtpd-option-configuration-not + opensmtpd-option-configuration-regex + opensmtpd-option-configuration-data + + opensmtpd-filter-phase-configuration + opensmtpd-filter-phase-configuration? + opensmtpd-filter-phase-configuration-name + opensmtpd-filter-phase-configuration-phase-name + opensmtpd-filter-phase-configuration-options + opensmtpd-filter-phase-configuration-decision + opensmtpd-filter-phase-configuration-message + opensmtpd-filter-phase-configuration-value + + opensmtpd-filter-configuration + opensmtpd-filter-configuration? + opensmtpd-filter-configuration-name + opensmtpd-filter-configuration-proc + + opensmtpd-listen-on-configuration + opensmtpd-listen-on-configuration? + opensmtpd-listen-on-configuration-interface + opensmtpd-listen-on-configuration-family + opensmtpd-listen-on-configuration-auth + opensmtpd-listen-on-configuration-auth-optional + opensmtpd-listen-on-configuration-filters + opensmtpd-listen-on-configuration-hostname + opensmtpd-listen-on-configuration-hostnames + opensmtpd-listen-on-configuration-mask-src + opensmtpd-listen-on-configuration-disable-dsn + opensmtpd-listen-on-configuration-pki + opensmtpd-listen-on-configuration-port + opensmtpd-listen-on-configuration-proxy-v2 + opensmtpd-listen-on-configuration-received-auth + opensmtpd-listen-on-configuration-senders + opensmtpd-listen-on-configuration-secure-connection + opensmtpd-listen-on-configuration-tag + + opensmtpd-listen-on-socket-configuration + opensmtpd-listen-on-socket-configuration? + opensmtpd-listen-on-socket-configuration-filters + opensmtpd-listen-on-socket-configuration-mask-src + opensmtpd-listen-on-socket-configuration-tag + + opensmtpd-match-configuration + opensmtpd-match-configuration? + opensmtpd-match-configuration-action + opensmtpd-match-configuration-options + + opensmtpd-smtp-configuration + opensmtpd-smtp-configuration? + opensmtpd-smtp-configuration-ciphers + opensmtpd-smtp-configuration-limit-max-mails + opensmtpd-smtp-configuration-limit-max-rcpt + opensmtpd-smtp-configuration-max-message-size + opensmtpd-smtp-configuration-sub-addr-delim character + + opensmtpd-srs-configuration + opensmtpd-srs-configuration? + opensmtpd-srs-configuration-key + opensmtpd-srs-configuration-backup-key + opensmtpd-srs-configuration-ttl-delay + + opensmtpd-queue-configuration + opensmtpd-queue-configuration? + opensmtpd-queue-configuration-compression + opensmtpd-queue-configuration-encryption + opensmtpd-queue-configuration-ttl-delay + opensmtpd-configuration opensmtpd-configuration? + opensmtpd-package + opensmtpd-config-file + opensmtpd-configuration-bounce + opensmtpd-configuration-listen-ons + opensmtpd-configuration-listen-on-socket + opensmtpd-configuration-includes + opensmtpd-configuration-matches + opensmtpd-configuration-mda-wrappers + opensmtpd-configuration-mta-max-deferred + opensmtpd-configuration-srs + opensmtpd-configuration-smtp + opensmtpd-configuration-queue + opensmtpd-service-type %default-opensmtpd-config-file @@ -1651,13 +1786,1888 @@ (define (generate-dovecot-documentation) ;;; OpenSMTPD. ;;; +;; some fieldnames have a default value of #f, which is ok. They cannot have a value of #t. +;; for example opensmtpd-table-configuration-data can be #f, BUT NOT true. +;; my/sanitize procedure tests values to see if they are of the right kind. +;; procedure false? is needed to allow fields like 'values' to be blank, (empty), or #f BUT also +;; have a value like a list of strings. +(define (false? var) + (eq? #f var)) + +;; this procedure takes in a var and a list of procedures. It loops through list of procedures passing in var to each. +;; if one procedure returns #t, the function returns true. Otherwise #f. +;; TODO for fun rewrite this using map +;; If I rewrote it in map, then it may help with sanitizing. +;; eg: I could then potentially easily sanitize vars with lambda procedures. +(define (is-value-right-type? var list-of-procedures record fieldname) + (if (null? list-of-procedures) + #f + (cond [(procedure? (car list-of-procedures)) + (if ((car list-of-procedures) var) + #t + (is-value-right-type? var (cdr list-of-procedures) record fieldname))] + [(and (sanitize-configuration? (car list-of-procedures)) + (sanitize-configuration-error-if-proc-fails (car list-of-procedures)) + (if ((sanitize-configuration-proc (car list-of-procedures)) var) + #t + (begin + (apply string-append + (sanitize-configuration-error-message (car list-of-procedures))) + (throw 'bad! var))))] + [else (if ((sanitize-configuration-proc (car list-of-procedures)) var) + #t + (is-value-right-type? var (cdr list-of-procedures) record fieldname))]))) + +;; converts strings like this: +;; "apple, ham, cherry" -> "apple, ham, or cherry" +;; "pineapple" -> "pinneapple". +;; "cheese, grapefruit, or jam" -> "cheese, grapefruit, or jam" +(define (add-comma-or string) + (define last-comma-location (string-rindex string #\,)) + (if last-comma-location + (if (string-contains string ", or" last-comma-location) + string + (string-replace string ", or" last-comma-location + (+ 1 last-comma-location))) + string)) + +;; I could test for read-ability of a file, but then I would have to +;; test the program as root everytime instead of as a normal user... +(define (file-exists? file) +(if (string? file) + (access? file F_OK) + #f)) + +(define (list-of-procedures->string procedures) + (define string + (let loop ([procedures procedures]) + (if (null? procedures) + "" + (begin + (string-append + (cond [(eq? false? (car procedures)) + "#f , "] + [(eq? boolean? (car procedures)) + "boolean, "] + [(eq? string? (car procedures)) + "string, "] + [(eq? integer? (car procedures)) + "integer, "] + [(eq? list-of-strings? (car procedures)) + "list of strings, "] + [(eq? assoc-list? (car procedures)) + "an association list, "] + [(eq? opensmtpd-pki-configuration? (car procedures)) + "an record, "] + [(eq? opensmtpd-table-configuration? (car procedures)) + "an record, "] + [(eq? list-of-unique-opensmtpd-match-configuration? (car procedures)) + "a list of unique records, "] + [(eq? table-whose-data-are-assoc-list? (car procedures)) + (string-append + "an record whose fieldname 'values' are an assoc-list \n" + "(eg: (opensmtpd-table-configuration (name \"table\") (data '(\"joshua\" . \"$encrypted$password\")))), ")] + [(eq? file-exists? (car procedures)) + "file, "] + [else "has an incorrect value, "]) + (loop (cdr procedures))))))) + (add-comma-or (string-append (string-drop-right string 2) ".\n"))) + +;; TODO can I M-x raise-sexp (string=? string var) in this procedure? and get rid of checking +;; if the var is a string? The previous string-in-list? had that check. +;; (string-in-list? '("hello" 5 "cat")) currently works. If I M-x raise-sexp (string=? string var) +;; then it will no longer work. +(define (string-in-list? string list) + (primitive-eval (cons 'or (map (lambda (var) (and (string? var) (string=? string var))) list)))) + +(define (my/sanitize var record fieldname list-of-procedures) + (if (is-value-right-type? var list-of-procedures record fieldname) + var + (begin + (display (string-append "<" record "> fieldname: '" fieldname "' is of type " + (list-of-procedures->string list-of-procedures) "\n")) + (throw 'bad! var)))) + +;; Some example opensmtpd-table-configurations: +;; +;; (opensmtpd-table-configuration (name "root accounts") (data '(("joshua" . "root@dismail.de") ("joshua" . "postmaster@dismail.de")))) +;; (opensmtpd-table-configuration (name "root accounts") (data (list "mysite.me" "your-site.com"))) +;; TODO should support have a fieldname 'file'? +;; Or should I change name to name-or-file ? +(define-record-type* + opensmtpd-table-configuration make-opensmtpd-table-configuration + opensmtpd-table-configuration? + this-record + (name opensmtpd-table-configuration-name ;; string + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-table-configuration" "name" (list string?))))) + (file-db opensmtpd-table-configuration-file-db + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-table-configuration" "file-db" + (list boolean?))))) + ;; FIXME support an aliasing table as described here: + ;; https://man.openbsd.org/table.5 + ;; One may have to use the record file for this. I don't think tables support a table like this: + ;; table "name" { joshua = joshua@gnucode.me,joshua@gnu-hurd.com,joshua@propernaming.org, root = root@gnucode.me } + ;; If values is an absolute filename, then it will use said filename to house the table info. + ;; filename must be an absolute filename. + (data opensmtpd-table-configuration-data + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-table-configuration" "values" + (list file-exists? list-of-strings? assoc-list?))))) + ;; is a list of values or key values + ;; eg: (list "mysite.me" "your-site.com") + ;; eg: (list ("joshua" . "joshua@gnu.org") ("james" . "james@gnu.org")) + ;; I am currently making these values be as assocation list of strings only. + ;; FIXME should I allow a var like this? + ;; (list (cons "gnucode.me" 234.949.392.23)) + ;; can be of type: (quote list-of-strings) or (quote assoc-list) + ;; (opensmtpd-table-configuration-type record) returns the values' type. The user SHOULD NEVER set the type. + ;; TODO jpoiret: on irc reccomends that I just use an outside function to determine fieldname 'values', type. + ;; it would be "simpler" and possibly easier for the next person working on this code to understand what is happening. + (type opensmtpd-table-configuration-type + (default #f) + (thunked) + (sanitize (lambda (var) + (cond [(opensmtpd-table-configuration-data this-record) + (if (list-of-strings? (opensmtpd-table-configuration-data this-record)) + (quote list-of-strings) + (quote assoc-list))] + [(file-exists? (opensmtpd-table-configuration-data this-record)) + (if (opensmtpd-table-configuration-file-db this-record) + (quote db) + (quote file))] + [else + (display "opensmtpd-table-configuration-type is broke\n") + (throw 'bad! var)]))))) + +(define-record-type* + opensmtpd-ca-configuration make-opensmtpd-ca-configuration + opensmtpd-ca-configuration? + (name opensmtpd-ca-configuration-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-ca-configuration" "name" (list string?))))) + (file opensmtpd-ca-configuration-file + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-ca-configuration" "file" (list file-exists?)))))) + +(define-record-type* + opensmtpd-pki-configuration make-opensmtpd-pki-configuration + opensmtpd-pki-configuration? + (domain opensmtpd-pki-configuration-domain + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-pki-configuration" "domain" (list string?))))) + ;; TODO/FIXME this should probably be a list of files. The opensmtpd documentation says + ;; that you could have a list of files: + ;; + ;; pki pkiname cert certfile + ;; Associate certificate file certfile with host pkiname, and use that file to prove + ;; the identity of the mail server to clients. pkiname is the server's name, de‐ + ;; rived from the default hostname or set using either + ;; /gnu/store/2d13sdz76ldq8zgwv4wif0zx7hkr3mh2-opensmtpd-6.8.0p2/etc/mailname or us‐ + ;; ing the hostname directive. If a fallback certificate or SNI is wanted, the ‘*’ + ;; wildcard may be used as pkiname. + + ;; A certificate chain may be created by appending one or many certificates, includ‐ + ;; ing a Certificate Authority certificate, to certfile. The creation of certifi‐ + ;; cates is documented in starttls(8). + (cert opensmtpd-pki-configuration-cert + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-pki-configuration" "cert" (list file-exists?))))) + (key opensmtpd-pki-configuration-key + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-pki-configuration" "key" (list file-exists?))))) + ; todo sanitize this. valid parameters are "none", "legacy", or "auto". + (dhe opensmtpd-pki-configuration-dhe + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-dhe" "dhe" (list false? string?)))))) + +(define-record-type* + opensmtpd-lmtp-configuration make-opensmtpd-lmtp-configuration + opensmtpd-lmtp-configuration? + (destination opensmtpd-lmtp-configuration-destination + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-lmtp-configuration" "destination" + (list string?))))) + (rcpt-to opensmtpd-lmtp-configuration-rcpt-to + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-lmtp-configuration" "rcpt-to" + (list false? string?)))))) + +(define-record-type* + opensmtpd-mda-configuration make-opensmtpd-mda-configuration + opensmtpd-mda-configuration? + (name opensmtpd-mda-configuration-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-mda-configuration" "name" + (list string?))))) + ;; TODO should I allow this command to be a gexp? + (command opensmtpd-mda-configuration-command + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-mda-configuration" "command" + (list string?)))))) + +(define-record-type* + opensmtpd-maildir-configuration make-opensmtpd-maildir-configuration + opensmtpd-maildir-configuration? + (pathname opensmtpd-maildir-configuration-pathname + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-maildir-configuration" "pathname" + (list false? string?))))) + (junk opensmtpd-maildir-configuration-junk + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-maildir-configuration" "junk" + (list boolean?)))))) + +(define-record-type* + opensmtpd-action-local-delivery-configuration make-opensmtpd-action-local-delivery-configuration + opensmtpd-action-local-delivery-configuration? + (name opensmtpd-action-local-delivery-configuration-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-local-delivery-configuration" "name" + (list string?))))) + (method opensmtpd-action-local-delivery-configuration-method + (default "mbox") + (sanitize (lambda (var) + (cond + [(or (opensmtpd-lmtp-configuration? var) + (opensmtpd-maildir-configuration? var) + (opensmtpd-mda-configuration? var) + (string=? var "mbox") + (string=? var "expand-only") + (string=? var "forward-only")) + var] + [else + (begin + (display (string-append " fieldname 'method' must be of type \n" + "\"mbox\", \"expand-only\", \"forward-only\" \n" + ", , \n" + "or .\n")) + (throw 'bad! var))])))) + (alias opensmtpd-action-local-delivery-configuration-alias + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-local-delivery-configuration" "alias" + (list false? opensmtpd-table-configuration?))))) + (ttl opensmtpd-action-local-delivery-configuration-ttl + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-local-delivery-configuration" "ttl" + (list false? string?))))) + (user opensmtpd-action-local-delivery-configuration-user + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-local-delivery-configuration" "user" + (list false? string?))))) + (userbase opensmtpd-action-local-delivery-configuration-userbase + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-local-delivery-configuration" "userbase" + (list false? opensmtpd-table-configuration?))))) + (virtual opensmtpd-action-local-delivery-configuration-virtual + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-local-delivery-configuration" "virtual" + (list false? opensmtpd-table-configuration?))))) + (wrapper opensmtpd-action-local-delivery-configuration-wrapper + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-local-delivery-configuration" "wrapper" + (list false? string?)))))) + +;; FIXME/TODO this is a valid opensmtpd-relay record +;; (opensmtpd-action-relay-configuration +;; (pki (opensmtpd-pki-configuration +;; (domain "gnucode.me") +;; (cert "opensmtpd.scm") +;; (key "opensmtpd.scm")))) +;; BUT how does it relay the email? What host does it use? +;; I think opensmtpd-relay-configuration needs "method" field. +;; the method field might need to be another record...BUT basically the relay has to have a 'backup', 'backup-mx', +;; or 'domain', or 'host' defined. +(define-record-type* + opensmtpd-action-relay-configuration make-opensmtpd-action-relay-configuration + opensmtpd-action-relay-configuration? + (name opensmtpd-action-relay-configuration-name + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "name" + (list string?)))) + (default #f)) + (backup opensmtpd-action-relay-configuration-backup ;; boolean + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "backup" + (list boolean?))))) + (backup-mx opensmtpd-action-relay-configuration-backup-mx ;; string mx name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "backup-mx" + (list false? string?))))) + (helo opensmtpd-action-relay-configuration-helo + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "helo" + (list false? string? opensmtpd-table-configuration?)))) + (default #f)) + (helo-src opensmtpd-action-relay-configuration-helo-src + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "helo-src" + (list false? string? opensmtpd-table-configuration?)))) + (default #f)) + (domain opensmtpd-action-relay-configuration-domain + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "domain" + (list false? opensmtpd-table-configuration?)))) + (default #f)) + (host opensmtpd-action-relay-configuration-host + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "host" + (list false? string?)))) + (default #f)) + (pki opensmtpd-action-relay-configuration-pki + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "pki" + (list false? opensmtpd-pki-configuration?))))) + (srs opensmtpd-action-relay-configuration-srs + (default #f) + (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "srs" + (list boolean?)))) + (tls opensmtpd-action-relay-configuration-tls + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "tls" + (list false? string?))))) + (auth opensmtpd-action-relay-configuration-auth + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "auth" + (list false? opensmtpd-table-configuration?)))) + (default #f)) + (mail-from opensmtpd-action-relay-configuration-mail-from + (default #f)) + ;; string "127.0.0.1" or "" or "
" + ;; TODO should I do some sanitizing to make sure that the string? here is actually an IP address or a valid interface? + (src opensmtpd-action-relay-configuration-src + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-action-relay-configuration" "src" + (list false? string? opensmtpd-table-configuration?)))) + (default #f))) + +;; this record is used by & +;; +(define-record-type* + opensmtpd-option-configuration make-opensmtpd-option-configuration + opensmtpd-option-configuration? + (option opensmtpd-option-configuration-option + (default #f) + (sanitize (lambda (var) + (if (and (string? var) + (or (string-in-list? var (list "fcrdns" "rdns" + "src" "helo" + "auth" "mail-from" + "rcpt-to" + "for" + "for any" "for local" + "for domain" "for rcpt-to" + "from any" "from auth" + "from local" "from mail-from" + "from rdns" "from socket" + "from src" "auth" + "helo" "mail-from" + "rcpt-to" "tag" "tls" + )))) + var + (begin + (display (string-append " fieldname: 'option' is of type \n" + "string. The string can be either 'fcrdns', \n" + " 'rdns', 'src', 'helo', 'auth', 'mail-from', or 'rcpt-to', \n" + "'for', 'for any', 'for local', 'for domain', 'for rcpt-to', \n" + "'from any', 'from auth', 'from local', 'from mail-from', 'from rdns', 'from socket', \n" + "'from src', 'auth helo', 'mail-from', 'rcpt-to', 'tag', or 'tls' \n" + )) + (throw 'bad! var)))))) + (not opensmtpd-option-configuration-not + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-option-configuration" "not" + (list boolean?))))) + (regex opensmtpd-option-configuration-regex + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-option-configuration" "regex" + (list boolean?))))) + (data opensmtpd-option-configuration-data + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-option-configuration" "data" + (list false? string? opensmtpd-table-configuration?)))))) + +(define-record-type* + opensmtpd-filter-phase-configuration make-opensmtpd-filter-phase-configuration + opensmtpd-filter-phase-configuration? + (name opensmtpd-filter-phase-configuration-name ;; string chain-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter-phase-configuration" "name" + (list string?))))) + (phase opensmtpd-filter-phase-configuration-phase ;; string + (default #f) + (sanitize (lambda (var) + ;;(my/sanitize var "opensmtpd-filter-phase-configuration" "phase" + ;; (list (sanitize-configuration + ;; (proc (lambda (value) + ;; (and (string? var) + ;; (string-in-list? var (list "connect" + ;; "helo" + ;; "mail-from" + ;; "rcpt-to" + ;; "data" + ;; "commit"))))) + ;; (error-message (list + ;; " fieldname: 'phase' is of type \n" + ;; "string. The string can be either 'connect'," + ;; " 'helo', 'mail-from', 'rcpt-to', 'data', or 'commit.'\n "))))) + (if (and (string? var) + (string-in-list? var (list "connect" + "helo" + "mail-from" + "rcpt-to" + "data" + "commit"))) + var + (begin + (display (string-append " fieldname: 'phase' is of type \n" + "string. The string can be either 'connect'," + " 'helo', 'mail-from', 'rcpt-to', 'data', or 'commit.'\n " + )) + (throw 'bad! var))) + ))) + + (options opensmtpd-filter-phase-configuration-options + (default #f) + (sanitize (lambda (var) + ;; returns #t if list is a unique list of + (define (list-of-opensmtpd-option-configuration? list) + (and (list-of-type? list opensmtpd-option-configuration?) + (not (contains-duplicate? list)))) + + (define (list-has-duplicates-or-non-opensmtpd-option-configuration list) + (not (list-of-opensmtpd-option-configuration? list))) + + ;; input + ;; return #t if fieldname 'option' + ;; that needs a corresponding table has one. Otherwise #f + (define (opensmtpd-option-configuration-has-table? record) + (define decision (opensmtpd-option-configuration-option record)) + (and (string? decision) + ;; if option needs a table, check for a table + (if (string-in-list? decision (list "src" + "helo" + "mail-from" + "rcpt-to")) + (opensmtpd-table-configuration? (opensmtpd-option-configuration-data record)) + #t))) + + (define (list-of-opensmtpd-option-configuration-has-table? list) + (list-of-type? list opensmtpd-option-configuration-has-table?)) + + (define (some-opensmtpd-option-configuration-in-list-lack-table? list) + (not (list-of-opensmtpd-option-configuration-has-table? list))) + + ;;each element in list is of type + (cond [(list-has-duplicates-or-non-opensmtpd-option-configuration var) + (begin + (display (string-append " fieldname: 'options' is a list of unique \n" + " records.\n")) + (throw 'bad! var))] + ;; if fieldname 'option' is of string 'src', 'helo', 'mail-from', 'rcpt-to', then there should be a table + [(some-opensmtpd-option-configuration-in-list-lack-table? var) + (begin + (display (string-append "'s fieldname 'option' values of \n" + "'src', 'helo', 'mail-from', or 'rcpt-to' need a corresponding 'table' \n" + " of type . eg: \n" + "(opensmtpd-option-configuration \n" + " (option \"src\")\n" + " (table (opensmtpd-table-configuration \n" + " (name \"src-table\")\n" + " (data (list \"hello\" \"cat\")))))\n")) + ;; TODO it would be nice if the var this error message throws in the bad + ;; , instead of the list of records. + (throw 'bad! var))] + [else var])))) + (decision opensmtpd-filter-phase-configuration-decision + (default #f) + (sanitize (lambda (var) + (if (and (string? var) + (string-in-list? var (list "bypass" "disconnect" + "reject" "rewrite" "junk"))) + var + (begin + (display (string-append " fieldname: 'decision' is of type \n" + "string. The string can be either 'bypass'," + " 'disconnect', 'reject', 'rewrite', or 'junk'.\n")) + (throw 'bad! var)))))) + (message opensmtpd-filter-phase-configuration-message + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter-phase-configuration" "message" + (list false? string?))))) + (value opensmtpd-filter-phase-configuration-value + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter-phase-configuration" "value" + (list false? number?)))))) + +(define-record-type* + opensmtpd-filter-configuration make-opensmtpd-filter-configuration + opensmtpd-filter-configuration? + (name opensmtpd-filter-configuration-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter" "name" + (list string?))))) + (exec opensmtpd-filter-exec + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter" "exec" + (list boolean?))))) + (proc opensmtpd-filter-configuration-proc ; a string like "rspamd" or the command to start it like "/path/to/rspamd --option=arg --2nd-option=arg2" + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter" "proc" + (list string?)))))) + +;; There is another type of filter that opensmtpd supports, which is a filter chain. +;; A filter chain is a list of and . +;; This lets you apply several filters under one filter name. I could have defined +;; a record type for it, but the record would only have had two fields: name and list-of-filters. +;; Why write that as a record? That's too simple. +;; returns #t if list is a unique list of or +;; returns # otherwise +(define (opensmtpd-filter-chain? %filters) + (and (list-of-unique-filter-or-filter-phase? %filters) + (< 1 (length %filters)))) + +(define-record-type* + opensmtpd-listen-on-configuration make-opensmtpd-listen-on-configuration + opensmtpd-listen-on-configuration? + ;; interface may be an IP address, interface group, or domain name + (interface opensmtpd-listen-on-configuration-interface + (default "lo")) + (family opensmtpd-listen-on-configuration-family + (default #f) + (sanitize (lambda (var) + (cond + [(eq? #f var) ;; var == #f + var] + [(and (string? var) + (string-in-list? var (list "inet4" "inet6"))) + var] + [else + (begin + (display " fieldname 'family' must be string \"inet4\" or \"inet6\".\n") + (throw 'bad! var))])))) + (auth opensmtpd-listen-on-configuration-auth + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-listen-on-configuration" "auth" + (list boolean? table-whose-data-are-assoc-list?))))) + (auth-optional opensmtpd-listen-on-configuration-auth-optional + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-listen-on-configuration" "auth-optional" + (list boolean? + table-whose-data-are-assoc-list?))))) + ;; TODO add a ca entry? + ;; string FIXME/TODO sanitize this to support a gexp. That way way the + ;; includes directive can include my hacky scheme code that I use for opensmtpd-dkimsign. + (filters opensmtpd-listen-on-configuration-filters + (default #f) + (sanitize (lambda (var) + (sanitize-filters var)))) + (hostname opensmtpd-listen-on-configuration-hostname + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-listen-on-configuration" "hostname" + (list false? string?))))) + (hostnames opensmtpd-listen-on-configuration-hostnames + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-listen-on-configuration" "hostnames" + (list false? table-whose-data-are-assoc-list?))))) + (mask-src opensmtpd-listen-on-configuration-mask-src + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-listen-on-configuration" "mask-src" + (list boolean?))))) + (disable-dsn opensmtpd-listen-on-configuration-disable-dsn + (default #f)) + (pki opensmtpd-listen-on-configuration-pki + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-listen-on-configuration" "pki" + (list false? opensmtpd-pki-configuration?))))) + (port opensmtpd-listen-on-configuration-port + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-listen-on-configuration" "port" + (list false? integer?))))) + (proxy-v2 opensmtpd-listen-on-configuration-proxy-k2 + (default #f)) + (received-auth opensmtpd-listen-on-configuration-received-auth + (default #f)) + ;; TODO add in a senders option! + ;; string or record + ;; (senders opensmtpd-listen-on-configuration-senders + ;; (sanitize (lambda (var) + ;; (my/sanitize var "opensmtpd-listen-on-configuration" "port" (list false? integer?)))) + ;; (default #f)) + (secure-connection opensmtpd-listen-on-configuration-secure-connection + (default #f) + (sanitize (lambda (var) + (cond [(boolean? var) + var] + [(and (string? var) + (string-in-list? var + (list "smtps" "tls" + "tls-require" + "tls-require-verify"))) + var] + [else + (begin + (display (string-append " fieldname 'secure-connection' can be \n" + "one of the following strings: \n'smtps', 'tls', 'tls-require', \n" + "or 'tls-require-verify'.\n")) + (throw 'bad! var))])))) + (tag opensmtpd-listen-on-configuration-tag + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-listen-on-configuration" "tag" + (list false? string?)))) + (default #f))) + +(define-record-type* + opensmtpd-listen-on-socket-configuration-configuration make-opensmtpd-listen-on-socket-configuration-configuration + opensmtpd-listen-on-socket-configuration-configuration? + ;; false or or list of + (filters opensmtpd-listen-on-socket-configuration-configuration-filters + (sanitize (lambda (var) + (sanitize-filters var))) + (default #f)) + (mask-src opensmtpd-listen-on-socket-configuration-configuration-mask-src + (default #f)) + (tag opensmtpd-listen-on-socket-configuration-configuration-tag + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-listen-on-configuration" "tag" + (list false? string?)))) + (default #f))) + + +(define-record-type* + opensmtpd-match-configuration make-opensmtpd-match-configuration + opensmtpd-match-configuration? + ;;TODO? Perhaps I should add in a reject fieldname. If reject + ;;is #t, then the match record will be a reject match record. + ;; (opensmtpd-match (reject #t)) vs. (opensmtpd-match (action 'reject)) + ;; To do this, I will also have to 'reject' mutually exclusive. AND an match with 'reject' can have no action defined. + (action opensmtpd-match-configuration-action + (default #f) + (sanitize (lambda (var) + (if (or (opensmtpd-action-relay-configuration? var) + (opensmtpd-action-local-delivery-configuration? var) + (eq? (quote reject) var)) + var + (begin + (display + (string-append " fieldname 'action' is of type , \n" + ", or (quote reject).\n" + "If its var is (quote reject), then the match rejects the incoming message\n" + "during the SMTP dialogue.\n")) + (throw 'bad! var)))))) + (options opensmtpd-match-configuration-options + (default #f) + (sanitize (lambda (var) + (cond ((not var) + #f) + ((not (list-of-unique-opensmtpd-option-configuration? var)) + (throw-error var '(" fieldname 'options' is a list of unique \n" + " records. \n"))) + (else (sanitize-list-of-options-for-match-configuration var))))))) + +(define-record-type* + opensmtpd-smtp-configuration make-opensmtpd-smtp-configuration + opensmtpd-smtp-configuration? + (ciphers opensmtpd-smtp-configuration-ciphers + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp-configuration" "ciphers" + (list false? string?))))) + (limit-max-mails opensmtpd-smtp-configuration-limit-max-mails + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp-configuration" "limit-max-mails" + (list false? integer?))))) + (limit-max-rcpt opensmtpd-smtp-configuration-limit-max-rcpt + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp-configuration" "limit-max-rcpt" + (list false? integer?))))) + (max-message-size opensmtpd-smtp-configuration-max-message-size + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp-configuration" "max-message-size" + (list false? integer? string?))))) + ;; FIXME/TODO the sanitize function of sub-addr-delim should accept a string of length one not string? + (sub-addr-delim opensmtpd-smtp-configuration-sub-addr-delim + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp-configuration" "sub-addr-delim" + (list false? integer? string?)))))) + +(define-record-type* + opensmtpd-srs-configuration make-opensmtpd-srs-configuration + opensmtpd-srs-configuration? + ;; TODO should this be a file? + (key opensmtpd-srs-configuration-key + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-srs-configuration" "key" + (list false? boolean? string?))))) + ;; TODO should this also be a file? + (backup-key opensmtpd-srs-configuration-backup-key + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-srs-configuration" "backup-key" + (list false? integer?))))) + (ttl-delay opensmtpd-srs-configuration-ttl-delay + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-srs-configuration" "ttl-delay" + (list false? string?)))))) + +(define-record-type* + opensmtpd-queue-configuration make-opensmtpd-queue-configuration + opensmtpd-queue-configuration? + (compression opensmtpd-queue-configuration-compression + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-queue-configuration" "compression" + (list boolean?))))) + (encryption opensmtpd-queue-configuration-encryption + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-queue-configuration" "encryption" + (list boolean? file-exists? string?))))) + (ttl-delay opensmtpd-queue-configuration-ttl-delay + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-queue-configuration" "ttl-delay" + (list false? string?)))))) + (define-record-type* opensmtpd-configuration make-opensmtpd-configuration opensmtpd-configuration? - (package opensmtpd-configuration-package - (default opensmtpd)) + (package opensmtpd-configuration-package + (default opensmtpd)) (config-file opensmtpd-configuration-config-file - (default %default-opensmtpd-config-file))) + (default #f)) + ;; FIXME/TODO should I include a admd authservid entry? + + ;; TODO sanitize this properly with perhaps a . + (bounce opensmtpd-configuration-bounce + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "bounce" + (list false? list?))))) + (cas opensmtpd-configuration-cas + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "cas" + (list false? list-of-opensmtpd-ca-configuration?))))) + ;; list of many records of type opensmtpd-listen-on-configuration + (listen-ons opensmtpd-configuration-listen-ons + (default (list (opensmtpd-listen-on-configuration))) + (sanitize (lambda (var) + (if (list-of-opensmtpd-listen-on-configuration? var) + var + (begin + (display " fieldname 'listen-ons' expects a list of records ") + (display "of one or more unique records.\n") + (throw 'bad! var)))))) + ;; accepts type + (listen-on-socket opensmtpd-configuration-listen-on-socket + (default (opensmtpd-listen-on-socket-configuration-configuration))) + (includes opensmtpd-configuration-includes ;; list of strings of absolute path names + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "includes" + (list false? list-of-strings?))))) + (matches opensmtpd-configuration-matches + (default (list (opensmtpd-match-configuration + (action (opensmtpd-action-local-delivery-configuration + (name "local") + (method "mbox"))) + (options (list + (opensmtpd-option-configuration + (option "for local"))))) + (opensmtpd-match-configuration + (action (opensmtpd-action-relay-configuration + (name "outbound"))) + (options (list + (opensmtpd-option-configuration + (option "from local")) + (opensmtpd-option-configuration + (option "for any"))))))) + ;; TODO perhaps I should sanitize this function like I sanitized the 'filters'. + ;; I definitely should sanitize this function a bit more. For example, you could have two different + ;; actions, one for local delivery and one for remote, with the same name. I should make sure that + ;; I have no two different actions with the same name. + (sanitize (lambda (var) + ;; Should we do more sanitizing here? eg: "from socket" should NOT have a table or value + var + (my/sanitize var "opensmtpd-configuration" "matches" + (list list-of-unique-opensmtpd-match-configuration?))))) + ;; list of many records of type mda-wrapper + ;; TODO/FIXME support using gexps here + ;; eg (list "name" gexp) + (mda-wrappers opensmtpd-configuration-mda-wrappers + (default #f) + (sanitize (lambda (var) + (my/sanitize var + "opensmtpd-configuration" + "mda-wrappers" + (list false? string?))))) + (mta-max-deferred opensmtpd-configuration-mta-max-deferred + (default 100) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "mta-max-deferred" + (list number?))))) + + ;; TODO should I add a fieldname proc _proc-name_ _command_ as found in the man 5 smtpd.conf ? + + (queue opensmtpd-configuration-queue + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "queue" + (list false? opensmtpd-queue-configuration?))))) + (smtp opensmtpd-configuration-smtp + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "smtp" + (list false? opensmtpd-smtp-configuration?))))) + (srs opensmtpd-configuration-srs + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "srs" + (list false? opensmtpd-srs-configuration?)))))) + +;; This is a non-exported record for passing around sanitize procedures. +;; As of 5/2/2022 I am not using it. I should probably just delete it. +(define-record-type* + sanitize-configuration make-sanitize-configuration + sanitize-configuration? + (proc sanitize-configuration-proc + (default #f) + ;;(sanitize (lambda (var) (procedure? var))) + ) + (args sanitize-configuration-args + (default #f) + ;;(sanitize (lambda (var) (lambda (var) (list? var)))) + ) + (error-message sanitize-configuration-error-message + (default #f) + ;;(sanitize (lambda (var) (list? var))) + ) + (error-if-proc-fails sanitize-configuration-error-if-proc-fails + (default #f))) + +;; this help procedure is used 3 or 4 times by sanitize-list-of-options-for-match-configuration +(define (throw-error-duplicate-option option error-arg) + (throw-error error-arg + (list "'s fieldname 'options' has two\n" + (string-append " records with fieldname 'option' with value '" option "'. \n") + (string-append "You can only have one option with value '" option "' in the options list.\n")))) + +;; this procedure sanitizes the fieldname opensmtpd-match-configuration-options +(define* (sanitize-list-of-options-for-match-configuration %options) + (let loop ([%traversing-options %options] + [%sanitized-options '()]) + (if (null? %traversing-options) + (remove false? + (list + (assoc-ref %sanitized-options "for") + (assoc-ref %sanitized-options "from") + (assoc-ref %sanitized-options "auth") + (assoc-ref %sanitized-options "helo") + (assoc-ref %sanitized-options "mail-from") + (assoc-ref %sanitized-options "rcpt-to") + (assoc-ref %sanitized-options "tag") + (assoc-ref %sanitized-options "tls"))) + (let* ((option-record (car %traversing-options)) + (option-string (opensmtpd-option-configuration-option option-record))) + (cond [(string=? "auth" option-string) + (if (assoc-ref %sanitized-options "auth") + (throw-error-duplicate-option "auth" %traversing-options) + (loop (cdr %traversing-options) (alist-cons "auth" option-record %sanitized-options)))] + [(string=? "helo" option-string) + (cond [(assoc-ref %sanitized-options "helo") + (throw-error-duplicate-option "helo" %traversing-options)] + [(not (opensmtpd-option-configuration-data option-record)) + (throw-error option-record + (list " with fieldname 'option' with value 'helo' \n" + "must have a 'data' of type string or .\n"))] + [else (loop (cdr %traversing-options) (alist-cons "helo" option-record %sanitized-options))])] + [(string=? "mail-from" option-string) + (cond ((assoc-ref %sanitized-options "mail-from") + (throw-error-duplicate-option "mail-from" %traversing-options)) + ((not (opensmtpd-option-configuration-data option-record)) + (throw-error option-record + (list " with fieldname 'option' with value 'mail-from' \n" + "must have a 'data' of type string or .\n"))) + (else (loop (cdr %traversing-options) (alist-cons "mail-from" option-record %sanitized-options))))] + [(string=? "rcpt-to" option-string) + (cond [(assoc-ref %sanitized-options "rcpt-to") + (throw-error-duplicate-option "rcpt-to" %traversing-options)] + [(not (opensmtpd-option-configuration-data option-record)) + (throw-error option-record + (list " with fieldname 'option' with value 'rcpt-to' \n" + "must have a 'data' of type string or .\n"))] + [else (loop (cdr %traversing-options) (alist-cons "rcpt-to" option-record %sanitized-options))])] + [(string=? "tag" option-string) + (cond ((assoc-ref %sanitized-options "tag") + (throw-error-duplicate-option "tag" %traversing-options)) + ((not (string? (opensmtpd-option-configuration-data option-record))) + (throw-error option-record + (list " with fieldname 'option' with value 'tag' \n" + "must have a 'data' of type string.\n"))) + (else (loop (cdr %traversing-options) (alist-cons "tag" option-record %sanitized-options))))] + [(string=? "tls" option-string) + (cond [(assoc-ref %sanitized-options "tls") + (throw-error-duplicate-option "tls" %traversing-options)] + [(or (opensmtpd-option-configuration-data option-record) + (opensmtpd-option-configuration-regex option-record)) + (throw-error option-record + (list " with fieldname 'option' with value 'tls' \n" + "cannot have a string or table 'data'.\n"))] + [else (loop (cdr %traversing-options) (alist-cons "tls" option-record %sanitized-options))])] + [(string=? "for" (substring option-string 0 3)) + (cond ((assoc-ref %sanitized-options "for") + (throw-error %options + `("'s fieldname 'options' can only have one 'for' option. \n" + "But '" ,option-string "' and '" + ,(opensmtpd-option-configuration-option (assoc-ref %sanitized-options "for")) "' are present.\n"))) + ((and (string-in-list? option-string (list "for any" "for local")) ; for any cannot have a data field. + (or (opensmtpd-option-configuration-data option-record) + (opensmtpd-option-configuration-regex option-record))) + (throw-error option-record + (list "When 's fieldname 'options' value is 'for any' \n" + "or 'for local', then its 'data' and 'regex' field must be #f. \n"))) + ((and (string-in-list? option-string (list "for domain" "for rcpt-to")) ; for domain must have a data field. + (not (opensmtpd-option-configuration-data option-record))) + (throw-error option-record + (list "When 's fieldname 'options' value is 'for domain' \n" + "or 'for rcpt-to', then its 'data' field must be a string or an \n" + " record.\n"))) + (else (loop (cdr %traversing-options) (alist-cons "for" option-record %sanitized-options))))] + [(string=? "from" (substring option-string 0 4)) + (cond ((assoc-ref %sanitized-options "from") + (throw-error %options + `("'s fieldname 'options' can only have one 'from' option. \n" + "But '" ,option-string "' and '" + ,(opensmtpd-option-configuration-option (assoc-ref %sanitized-options "from")) "' are present.\n"))) + ((and (string-in-list? option-string (list "from any" "from local" "from socket")) ; for any cannot have a data field. + (or (opensmtpd-option-configuration-data option-record) + (opensmtpd-option-configuration-regex option-record))) + (throw-error option-record + (list "When 's fieldname 'options' value is 'from any', \n" + " 'from local', or 'from socket', then its 'data' and 'regex' field must be #f. \n"))) + ((and (string-in-list? option-string (list "from mail-from" "from src")) ; for domain must have a data field. + (not (opensmtpd-option-configuration-data option-record))) + (throw-error option-record + (list "When 's fieldname 'options' value is 'from mail-from' \n" + "or 'from src', then its 'data' field must be a string or an \n" + " record.\n"))) + (else (loop (cdr %traversing-options) (alist-cons "from" option-record %sanitized-options))))]))))) + +;; some procedures for and +;; . +(define (sanitize-filters %list) + ;; the order of the first two tests in this cond is important. + ;; (false?) has to be 1st and (list-has-duplicates-or-non-filters?) has to be second. + ;; You may optionally re-order the other alternates in the cond. + (cond [(false? %list) + #f] + [(list-has-duplicates-or-non-filters? %list) + (begin + (display (string-append " fieldname: 'filters' is a list, in which each unique element \n" + "is of type or .\n")) + (throw 'bad! %list))] + [else + (let loop ([%traversing-list %list] + [%original-list %list]) + (if (null? %traversing-list) + %original-list + (cond + [(opensmtpd-filter-configuration? (car %traversing-list)) + (loop (cdr %traversing-list) %original-list)] + [(filter-phase-has-message-and-value? (car %traversing-list)) + (begin + (display (string-append " cannot have defined fieldnames 'value' \n" + "and 'message'.\n")) + (throw 'bad! (car %traversing-list)))] + [(filter-phase-decision-lacks-proper-message? (car %traversing-list)) + (begin + (display (string-append " fieldname: 'decision' options \n" + "\"disconnect\" and \"reject\" require fieldname 'message' to have a string.\n" + "The 'message' string must be RFC commpliant, which means that the string \n" + "must begin with a 4xx or 5xx status code.\n")) + (throw 'bad! (car %traversing-list)))] + [(filter-phase-lacks-proper-value? (car %traversing-list)) + (begin + (display (string-append " fieldname: 'decision' option \n" + "\"rewrite\" requires fieldname 'value' to have a number.\n")) + (throw 'bad! (car %traversing-list)))] + [(filter-phase-has-incorrect-junk-or-bypass? (car %traversing-list)) + (begin + (display (string-append " fieldname 'decision' option \n" + "\"junk\" or 'bypass' cannot have a defined fieldnames 'message' or 'value'.\n")) + (throw 'bad! (car %traversing-list)))] + [(filter-phase-junks-after-commit? (car %traversing-list)) + (begin + (display (string-append " fieldname 'decision' option \n" + "\"junk\" cannot junk an email during 'phase' \"commit\".\n")) + (throw 'bad! (car %traversing-list)))] + [else (loop (cdr %traversing-list) %original-list)])))])) + +(define (list-has-duplicates-or-non-filters? list) + (not (list-of-unique-filter-or-filter-phase? list))) + +(define (filter-phase-has-message-and-value? record) + (and (opensmtpd-filter-phase-configuration-message record) + (opensmtpd-filter-phase-configuration-value record))) + +;; return #t if phase needs a message. Or if the message did not start with a 4xx or 5xx status code. +;; otherwise #f +(define (filter-phase-decision-lacks-proper-message? record) + (define decision (opensmtpd-filter-phase-configuration-decision record)) + (if (string-in-list? decision (list "disconnect" "reject")) + ;; this message needs to be RFC compliant, meaning + ;; that it need to start with 4xx or 5xx status code + (cond [(eq? #f (opensmtpd-filter-phase-configuration-message record)) + #t] + [(string? (opensmtpd-filter-phase-configuration-message record)) + (let ((number (string->number + (substring + (opensmtpd-filter-phase-configuration-message record) 0 3)))) + (if (and (number? number) + (and (< number 600) (> number 399))) + #f + #t))]) + #f)) + +;; 'decision' "rewrite" requires 'value' to be a number. +(define (filter-phase-lacks-proper-value? record) + (define decision (opensmtpd-filter-phase-configuration-decision record)) + (if (string=? "rewrite" decision) + (if (and (number? (opensmtpd-filter-phase-configuration-value record)) + (eq? #f (opensmtpd-filter-phase-configuration-message record))) + #f + #t) + #f)) + +;; 'decision' "junk" or "bypass" cannot have a message or a value. +(define (filter-phase-has-incorrect-junk-or-bypass? record) + (and + (string-in-list? + (opensmtpd-filter-phase-configuration-decision record) + (list "junk" "bypass")) + (or + (opensmtpd-filter-phase-configuration-value record) + (opensmtpd-filter-phase-configuration-message record)))) + +(define (filter-phase-junks-after-commit? record) + (and (string=? (opensmtpd-filter-phase-configuration-decision record) "junk") + (string=? (opensmtpd-filter-phase-configuration-phase record) "commit"))) + +;; returns #t if list is a unique list of or +;; returns # otherwise +(define (list-of-unique-filter-or-filter-phase? %filters) + (and (list? %filters) + (not (null? %filters)) + ;; this list is made up of only or + (primitive-eval + (cons 'and (map (lambda (filter) + (or (opensmtpd-filter-configuration? filter) + (opensmtpd-filter-phase-configuration? filter))) + %filters))) + (not (contains-duplicate? %filters)))) + +(define (throw-error var %strings) + (display (apply string-append %strings)) + (throw 'bad! var)) + +;; this is used for sanitizing fieldname 'options' +(define (contains-duplicate? list) + (if (null? list) + #f + (or + ;; check if (car list) is in (cdr list) + (primitive-eval (cons 'or + (map (lambda (var) (equal? var (car list))) + (cdr list)))) + ;; check if (cdr list) contains duplicate + (contains-duplicate? (cdr list))))) + +;; given a list and procedure, this tests that each element of list is of type +;; ie: (list-of-type? list string?) tests each list is of type string. +(define (list-of-type? list proc?) + (if (and (list? list) + (not (null? list))) + (let loop ([list list]) + (if (null? list) + #t + (if (proc? (car list)) + (loop (cdr list)) + #f))) + #f)) + +(define (list-of-strings? list) + (list-of-type? list string?)) + +(define (list-of-unique-opensmtpd-option-configuration? list) + (and (list-of-type? + list opensmtpd-option-configuration?) + (not (contains-duplicate? list)))) + +(define (list-of-opensmtpd-ca-configuration? list) + (list-of-type? list opensmtpd-ca-configuration?)) + +(define (list-of-opensmtpd-pki-configuration? list) + (list-of-type? list opensmtpd-pki-configuration?)) + +(define (list-of-opensmtpd-listen-on-configuration? list) + (and (list-of-type? list opensmtpd-listen-on-configuration?) + (not (contains-duplicate? list)))) + +(define (list-of-unique-opensmtpd-match-configuration? list) + (and (list-of-type? list opensmtpd-match-configuration?) + (not (contains-duplicate? list)))) + +(define* (list-of-strings->string list + #:key + (string-delimiter ", ") + (postpend "") + (append "") + (drop-right-number 2)) + (string-drop-right + (string-append (let loop ([list list]) + (if (null? list) + "" + (string-append append (car list) postpend + string-delimiter + (loop (cdr list))))) + append) + drop-right-number)) + +;; at the moment I cannot define this by using list-of-type? +;; the first (not (null? assoc-list)) prevents that. +(define (assoc-list? assoc-list) + (list-of-type? assoc-list (lambda (pair) + (if (and (pair? pair) + (string? (car pair)) + (string? (cdr pair))) + #t + #f)))) + +(define* (variable->string var #:key (append "") (postpend " ")) + (let ([var (if (number? var) + (number->string var) + var)]) + (if var + (string-append append var postpend) + ""))) + +;; this procedure takes in one argument. +;; if that argument is an whose fieldname 'values' is an assoc-list, then it returns +;; #t, #f if otherwise. +;; TODO should I remove these two functions? And instead use the (opensmtpd-table-configuration-type) procedure? +(define (table-whose-data-are-assoc-list? table) + (if (not (opensmtpd-table-configuration? table)) + #f + (assoc-list? (opensmtpd-table-configuration-data table)))) + +;; this procedure takes in one argument +;; if that argument is an whose fieldname 'values' is a list of strings, then it returns +;; #t, #f if otherwise. +(define (table-whose-data-are-a-list-of-strings? table) + (if (not (opensmtpd-table-configuration? table)) + #f + (list-of-strings? (opensmtpd-table-configuration-data table)))) + +;; these next few functions help me to turn
s +;; into strings suitable to fit into "opensmtpd.conf". +(define (assoc-list->string assoc-list) + (string-drop-right + (let loop ([assoc-list assoc-list]) + (if (null? assoc-list) + "" + ;; pair is (cons "hello" "world") -> ("hello" . "world") + (let ([pair (car assoc-list)]) + (string-append + "\"" (car pair) "\"" + " = " + "\"" (cdr pair) "\"" + ", " + (loop (cdr assoc-list)))))) + 2)) + +;; can be of type: (quote list-of-strings) or (quote assoc-list) +(define (opensmtpd-table-configuration->string table) + (string-append "table " (opensmtpd-table-configuration-name table) " " + (let ([type (opensmtpd-table-configuration-type table)]) + (cond [(eq? type (quote list-of-strings)) + (string-append "{ " (list-of-strings->string (opensmtpd-table-configuration-data table) + #:append "\"" + #:drop-right-number 3 + #:postpend "\"") " }")] + [(eq? type (quote assoc-list)) + (string-append "{ " (assoc-list->string (opensmtpd-table-configuration-data table)) " }")] + [(eq? type (quote db)) + (string-append "db:" (opensmtpd-table-configuration-data table))] + [(eq? type (quote file)) + (string-append "file:" (opensmtpd-table-configuration-data table))] + [else (throw 'youMessedUp table)])) + " \n")) + +;; The following functions convert various records into strings. + +(define (opensmtpd-listen-on-configuration->string record) + (string-append "listen on " + (opensmtpd-listen-on-configuration-interface record) " " + (let* ([hostname (opensmtpd-listen-on-configuration-hostname record)] + [hostnames (if (opensmtpd-listen-on-configuration-hostnames record) + (opensmtpd-table-configuration-name (opensmtpd-listen-on-configuration-hostnames record)) + #f)] + [filters (opensmtpd-listen-on-configuration-filters record)] + [filter-name (if filters + (if (< 1 (length filters)) + (generate-filter-chain-name filters) + (if (opensmtpd-filter-configuration? (car filters)) + (opensmtpd-filter-configuration-name (car filters)) + (opensmtpd-filter-phase-configuration-name (car filters)))) + #f)] + [mask-src (opensmtpd-listen-on-configuration-mask-src record)] + [tag (opensmtpd-listen-on-configuration-tag record)] + [secure-connection (opensmtpd-listen-on-configuration-secure-connection record)] + [port (opensmtpd-listen-on-configuration-port record)] + [pki (opensmtpd-listen-on-configuration-pki record)] + [auth (opensmtpd-listen-on-configuration-auth record)] + [auth-optional (opensmtpd-listen-on-configuration-auth-optional record)]) + (string-append + (if mask-src + (string-append "mask-src ") + "") + (variable->string hostname #:append "hostname ") + (variable->string hostnames #:append "hostnames <" #:postpend "> ") + (variable->string filter-name #:append "filter \"" #:postpend "\" ") + (variable->string tag #:append "tag \"" #:postpend "\" ") + (if secure-connection + (cond [(string=? "smtps" secure-connection) + "smtps "] + [(string=? "tls" secure-connection) + "tls "] + [(string=? "tls-require" secure-connection) + "tls-require "] + [(string=? "tls-require-verify" secure-connection) + "tls-require verify "]) + "") + (variable->string port #:append "port " #:postpend " ") + (if pki + (variable->string (opensmtpd-pki-configuration-domain pki) #:append "pki ") + "") + (if auth + (string-append "auth " + (if (opensmtpd-table-configuration? auth) + (string-append "<" (opensmtpd-table-configuration-name auth) "> ") + "")) + "") + (if auth-optional + (string-append "auth-optional " + (if (opensmtpd-table-configuration? auth-optional) + (string-append "<" (opensmtpd-table-configuration-name auth-optional) "> ") + "")) + "") + "\n")))) + +(define (opensmtpd-listen-on-socket-configuration->string record) + (string-append "listen on socket " + (let* ([filters (opensmtpd-listen-on-socket-configuration-configuration-filters record)] + [filter-name (if filters + (if (< 1 (length filters)) + (generate-filter-chain-name filters) + (if (opensmtpd-filter-configuration? (car filters)) + (opensmtpd-filter-configuration-name (car filters)) + (opensmtpd-filter-phase-configuration-name (car filters)))) + #f)] + [mask-src (opensmtpd-listen-on-socket-configuration-configuration-mask-src record)] + [tag (opensmtpd-listen-on-socket-configuration-configuration-tag record)]) + (string-append + (if mask-src + (string-append "mask-src ") + "") + (variable->string filter-name #:append "filter \"" #:postpend "\" ") + (variable->string tag #:append "tag \"" #:postpend "\" ") + "\n")))) + +(define (opensmtpd-action-relay-configuration->string record) + (let ([backup (opensmtpd-action-relay-configuration-backup record)] + [backup-mx (opensmtpd-action-relay-configuration-backup-mx record)] + [helo (opensmtpd-action-relay-configuration-helo record)] + ;; helo-src can either be a string IP address or an + [helo-src (if (opensmtpd-action-relay-configuration-helo-src record) + (if (string? (opensmtpd-action-relay-configuration-helo-src record)) + (opensmtpd-action-relay-configuration-helo-src record) + (string-append "<\"" + (opensmtpd-table-configuration-name + (opensmtpd-action-relay-configuration-src record)) + "\">")) + #f)] + [domain (if (opensmtpd-action-relay-configuration-domain record) + (opensmtpd-table-configuration-name + (opensmtpd-action-relay-configuration-domain record)) + #f)] + [host (opensmtpd-action-relay-configuration-host record)] + [name (opensmtpd-action-relay-configuration-name record)] + [pki (if (opensmtpd-action-relay-configuration-pki record) + (opensmtpd-pki-configuration-domain (opensmtpd-action-relay-configuration-pki record)) + #f)] + [srs (opensmtpd-action-relay-configuration-srs record)] + [tls (opensmtpd-action-relay-configuration-tls record)] + [auth (if (opensmtpd-action-relay-configuration-auth record) + (opensmtpd-table-configuration-name + (opensmtpd-action-relay-configuration-auth record)) + #f)] + [mail-from (opensmtpd-action-relay-configuration-mail-from record)] + ;; src can either be a string IP address or an + [src (if (opensmtpd-action-relay-configuration-src record) + (if (string? (opensmtpd-action-relay-configuration-src record)) + (opensmtpd-action-relay-configuration-src record) + (string-append "<\"" + (opensmtpd-table-configuration-name + (opensmtpd-action-relay-configuration-src record)) + "\">")) + #f)] + ) + (string-append + "\"" + name + "\" " "relay " + ;;FIXME should I always quote the host fieldname? do I need to quote localhost via "localhost" ? + (variable->string host #:append "host \"" #:postpend "\" ") + (variable->string backup) + (variable->string backup-mx #:append "backup mx ") + (variable->string helo #:append "helo ") + (variable->string helo-src #:append "helo-src ") + (variable->string domain #:append "domain <\"" #:postpend "\"> ") + (variable->string host #:append "host ") + (variable->string pki #:append "pki ") + (variable->string srs) + (variable->string tls #:append "tls ") + (variable->string auth #:append "auth <" #:postpend "> ") + (variable->string mail-from #:append "mail-from ") + (variable->string src #:append "src ") + "\n"))) + +(define (opensmtpd-lmtp-configuration->string record) + (string-append "lmtp " + (opensmtpd-lmtp-configuration-destination record) + (if (opensmtpd-lmtp-configuration-rcpt-to record) + (begin + " " (opensmtpd-lmtp-configuration-rcpt-to record)) + ""))) + +(define (opensmtpd-mda-configuration->string record) + (string-append "mda " + (opensmtpd-mda-configuration-command record) " ")) + +(define (opensmtpd-maildir-configuration->string record) + (string-append "maildir " + "\"" + (if (opensmtpd-maildir-configuration-pathname record) + (opensmtpd-maildir-configuration-pathname record) + "~/Maildir") + "\"" + (if (opensmtpd-maildir-configuration-junk record) + " junk " + " "))) + +(define (opensmtpd-action-local-delivery-configuration->string record) + (let ([name (opensmtpd-action-local-delivery-configuration-name record)] + [method (opensmtpd-action-local-delivery-configuration-method record)] + [alias (if (opensmtpd-action-local-delivery-configuration-alias record) + (opensmtpd-table-configuration-name + (opensmtpd-action-local-delivery-configuration-alias record)) + #f)] + [ttl (opensmtpd-action-local-delivery-configuration-ttl record)] + [user (opensmtpd-action-local-delivery-configuration-user record)] + [userbase (if (opensmtpd-action-local-delivery-configuration-userbase record) + (opensmtpd-table-configuration-name + (opensmtpd-action-local-delivery-configuration-userbase record)) + #f)] + [virtual (if (opensmtpd-action-local-delivery-configuration-virtual record) + (opensmtpd-table-configuration-name + (opensmtpd-action-local-delivery-configuration-virtual record)) + #f)] + [wrapper (opensmtpd-action-local-delivery-configuration-wrapper record)]) + (string-append + "\"" name "\" " + (cond [(string? method) + (string-append method " ")] + [(opensmtpd-mda-configuration? method) + (opensmtpd-mda-configuration->string method)] + [(opensmtpd-lmtp-configuration? method) + (opensmtpd-lmtp-configuration->string method)] + [(opensmtpd-maildir-configuration? method) + (opensmtpd-maildir-configuration->string method)]) + ;; FIXME/TODO support specifying alias file:/path/to/alias-file ? + ;; I do not think that is something that I can do... + (variable->string alias #:append "alias <\"" #:postpend "\"> ") + (variable->string ttl #:append "ttl ") + (variable->string user #:append "user ") + (variable->string userbase #:append "userbase <\"" #:postpend "\"> ") + (variable->string virtual #:append "virtual <" #:postpend "> ") + (variable->string wrapper #:append "wrapper ")))) + +;; this function turns both opensmtpd-action-local-delivery-configuration and +;; opensmtpd-action-relay-configuration into strings. +(define (opensmtpd-action->string record) + (string-append "action " + (cond [(opensmtpd-action-local-delivery-configuration? record) + (opensmtpd-action-local-delivery-configuration->string record)] + [(opensmtpd-action-relay-configuration? record) + (opensmtpd-action-relay-configuration->string record)]) + " \n")) + +;; this turns option records found in into strings. +(define* (opensmtpd-option-configuration->string record + #:key + (space-after-! #f)) + (let ([not (opensmtpd-option-configuration-not record)] + [option (opensmtpd-option-configuration-option record)] + [regex (opensmtpd-option-configuration-regex record)] + [data (opensmtpd-option-configuration-data record)]) + (string-append + (if not + (if space-after-! + "! " + "!") + "") + option " " + (if regex + "regex " + "") + (if data + (if (opensmtpd-table-configuration? data) + (string-append "<" (opensmtpd-table-configuration-name data) "> ") + (string-append data " ")) + "")))) + +(define (opensmtpd-match-configuration->string record) + (string-append "match " + (let* ([action (opensmtpd-match-configuration-action record)] + [name (cond [(opensmtpd-action-relay-configuration? action) + (opensmtpd-action-relay-configuration-name action)] + [(opensmtpd-action-local-delivery-configuration? action) + (opensmtpd-action-local-delivery-configuration-name action)] + [else 'reject])] + [options (opensmtpd-match-configuration-options record)]) + (string-append + (if options + (apply string-append + (map opensmtpd-option-configuration->string options)) + "") + (if (string? name) + (string-append "action " "\"" name "\" ") + "reject ") + "\n")))) + +(define (opensmtpd-ca-configuration->string record) + (string-append "ca " (opensmtpd-ca-configuration-name record) " " + "cert \"" (opensmtpd-ca-configuration-file record) "\"\n")) + +(define (opensmtpd-pki-configuration->string record) + (let ([domain (opensmtpd-pki-configuration-domain record)] + [cert (opensmtpd-pki-configuration-cert record)] + [key (opensmtpd-pki-configuration-key record)] + [dhe (opensmtpd-pki-configuration-dhe record)]) + (string-append "pki " domain " " "cert \"" cert "\" \n" + "pki " domain " " "key \"" key "\" \n" + (if dhe + (string-append + "pki " domain " " "dhe " dhe "\n") + "")))) + +(define (generate-filter-chain-name list-of-filters) + (string-drop-right (apply string-append + (flatten + (map (lambda (filter) + (list + (if (opensmtpd-filter-configuration? filter) + (opensmtpd-filter-configuration-name filter) + (opensmtpd-filter-phase-configuration-name filter)) + "-")) + list-of-filters))) + 1)) + +;; this procedure takes in a list of and , +;; returns a string of the form: +;; filter "uniquelyGeneratedName" chain chain { "filter-name", "filter-name2" [, ...]} +(define (opensmtpd-filter-chain->string list-of-filters) + (string-append "filter \"" + (generate-filter-chain-name list-of-filters) + "\" " + "chain {" + (string-drop-right + (apply string-append + (flatten + (map (lambda (filter) + (list + "\"" + (if (opensmtpd-filter-configuration? filter) + (opensmtpd-filter-configuration-name filter) + (opensmtpd-filter-phase-configuration-name filter)) + "\", ")) + list-of-filters)) + ) 2) + "}\n")) + +(define (opensmtpd-filter-phase-configuration->string record) + (let ([name (opensmtpd-filter-phase-configuration-name record)] + [phase (opensmtpd-filter-phase-configuration-phase record)] + [decision (opensmtpd-filter-phase-configuration-decision record)] + [options (opensmtpd-filter-phase-configuration-options record)] + [message (opensmtpd-filter-phase-configuration-message record)] + [value (opensmtpd-filter-phase-configuration-value record)]) + (string-append "filter " + "\"" name "\" " + "phase " phase " " + "match " + (apply string-append ; turn the options into a string + (flatten + (map (lambda (option) + (opensmtpd-option-configuration->string option #:space-after-! #f)) + options))) + " " + decision " " + (if (string-in-list? decision (list "reject" "disconnect")) + (string-append "\"" message "\"") + "") + (if (string=? "rewrite" decision) + (string-append "rewrite " (number->string value)) + "") + "\n"))) + +;; filters elements may be , , +;; and lists that look like (list (opensmtpd-filter-configuration...) (opensmtpd-filter-phase-configuration ...) +;; ...) +;; this function converts it to a string. +;; Consider if a user passed in a valid , whose total valid filters +;; so that (get-opensmtpd-filters (opensmtpd-configuration)) returns +;; look like this: (we will call this list "total filters"): +;; (list (opensmtpd-filter +;; (name "rspamd") +;; (proc "rspamd")) +;; (list (opensmtpd-filter-phase-configuration ; this is a listen-on, with a filter-chain. +;; (name "dkimsign") +;; ...) +;; (opensmtpd-filter +;; (name "rspamd") +;; (proc "rspamd")))) +;; +;; did you notice that filter "rspamd" is listed twice? How do you make sure that it is NOT +;; printed twice in smtpd.conf? +;; 1st flatten "total filters", then remove its duplicates. Then print all of those filters. +;; 2nd now we go through "total filters", and we only print the non-filter-chains. +(define (opensmtpd-filters->string filters) + ;; first display the unique s. and s. + ;; to do this: flatten filters, then remove duplicates. + (string-append + (apply string-append + (map (lambda (filter) + (cond ((opensmtpd-filter-phase-configuration? filter) + (opensmtpd-filter-phase-configuration->string filter)) + (else ; you are a + (string-append "filter " + "\"" (opensmtpd-filter-configuration-name filter) "\" " + (if (opensmtpd-filter-exec filter) + "proc-exec " + "proc ") + "\"" (opensmtpd-filter-configuration-proc filter) "\"" + "\n")))) + (delete-duplicates (flatten filters)))) + ;; now we have to print the filter chains. + (apply string-append + (remove boolean? + (map (lambda (filter) + (cond ((list? filter) + (opensmtpd-filter-chain->string filter)) + (else ; you are a + #f))) + filters))))) + +(define (opensmtpd-configuration-listen->string string) + (string-append + "include \"" string "\"\n")) + +(define (opensmtpd-configuration-srs->string record) + (let ([key (opensmtpd-srs-configuration-key record)] + [backup-key (opensmtpd-srs-configuration-backup-key record)] + [ttl-delay (opensmtpd-srs-configuration-ttl-delay record)]) + (string-append + (variable->string key #:append "srs key " #:postpend "\n") + (variable->string backup-key #:append "srs key backup " #:postpend "\n") + (variable->string ttl-delay #:append "srs ttl " #:postpend "\n") + "\n"))) + +;; TODO make sure all options here work! I just fixed limit-max-rcpt! +(define (opensmtpd-smtp-configuration->string record) + (let ([ciphers (opensmtpd-smtp-configuration-ciphers record)] + [limit-max-mails (opensmtpd-smtp-configuration-limit-max-mails record)] + [limit-max-rcpt (opensmtpd-smtp-configuration-limit-max-rcpt record)] + [max-message-size (opensmtpd-smtp-configuration-max-message-size record)] + [sub-addr-delim (opensmtpd-smtp-configuration-sub-addr-delim record)]) + (string-append + (variable->string ciphers #:append "smtp ciphers " #:postpend "\n") + (variable->string limit-max-mails #:append "smtp limit max-mails " #:postpend "\n") + (variable->string limit-max-rcpt #:append "smtp limit max-rcpt " #:postpend "\n") + (variable->string max-message-size #:append "smtp max-message-size " #:postpend "\n") + (variable->string sub-addr-delim #:append "smtp sub-addr-delim " #:postpend "\n") + "\n"))) + +(define (opensmtpd-configuration-queue->string record) + (let ([compression (opensmtpd-queue-configuration-compression record)] + [encryption (opensmtpd-queue-configuration-encryption record)] + [ttl-delay (opensmtpd-queue-configuration-ttl-delay record)]) + (string-append + (if compression + "queue compression\n" + "") + (if encryption + (string-append + "queue encryption " + (if (not (boolean? encryption)) + encryption + "") + "\n") + "") + (if ttl-delay + (string-append "queue ttl" ttl-delay "\n") + "")))) + +;; build a list of from +;; opensmtpd-configuration-matches, which is a list of . +;; Each has a fieldname 'action', which accepts an . +(define (get-opensmtpd-actions record) + (define opensmtpd-actions + (let loop ([list (opensmtpd-configuration-matches record)]) + (if (null? list) + '() + (cons (opensmtpd-match-configuration-action (car list)) + (loop (cdr list)))))) + (delete-duplicates (append opensmtpd-actions))) + +;; build a list of opensmtpd-pki-configurations from +;; opensmtpd-configuration-listen-ons and +;; get-opensmtpd-actions +(define (get-opensmtpd-pki-configurations record) + ;; TODO/FIXME/maybe/wishlist could get-opensmtpd-actions -> NOT have an opensmtpd-action-relay-configuration? + ;; I think so. And if it did NOT have a relay configuration, then action-pkis would be '() when + ;; it needs to be #f. because if the opensmtpd-configuration has NO pkis, then this function will + ;; return '(), when it should return #f. If it returns '(), then opensmtpd-configuration-fieldname->string will + ;; print the string "\n" instead of "" + (define action-pkis + (let loop1 ([list (get-opensmtpd-actions record)]) + (if (null? list) + '() + (if (and (opensmtpd-action-relay-configuration? (car list)) + (opensmtpd-action-relay-configuration-pki (car list))) + (cons (opensmtpd-action-relay-configuration-pki (car list)) + (loop1 (cdr list))) + (loop1 (cdr list)))))) + ;; FIXME/TODO/maybe/wishlist + ;; this could be #f aka left blank. aka there are no listen-ons records with pkis. + ;; aka there are no lines in the configuration like: + ;; listen on eth0 tls pki smtp.gnucode.me in that case the smtpd.conf will have an extra "\n" + (define listen-on-pkis + (let loop2 ([list (opensmtpd-configuration-listen-ons record)]) + (if (null? list) + '() + (if (opensmtpd-listen-on-configuration-pki (car list)) + (cons (opensmtpd-listen-on-configuration-pki (car list)) + (loop2 (cdr list))) + (loop2 (cdr list)))))) + (delete-duplicates (append action-pkis listen-on-pkis))) + +;; takes in a and returns a list whose elements are , +;; , and a filter-chain. +;; It returns a list of and/or +;; here's an example of what this procedure might return: +;; (list (opensmtpd-filter-configuration...) (opensmtpd-filter-phase-configuration ...) +;; (openmstpd-filter ...) (opensmtpd-filter-phase-configuration ...) +;; ;; this next list is a filter-chain. +;; (list (opensmtpd-filter-phase-configuration ...) (opensmtpd-filter-configuration...))) +;; +;; This procedure handles filter chains a little odd. +(define (get-opensmtpd-filters record) + (define list-of-listen-on-records (if (opensmtpd-configuration-listen-ons record) + (opensmtpd-configuration-listen-ons record) + '())) + + (define listen-on-socket-filters + (if (opensmtpd-listen-on-socket-configuration-configuration-filters (opensmtpd-configuration-listen-on-socket record)) + (opensmtpd-listen-on-socket-configuration-configuration-filters (opensmtpd-configuration-listen-on-socket record)) + '())) + + (delete-duplicates + (append (remove boolean? + (map-in-order (lambda (listen-on-record) ; get the filters found in the s + (if (and (opensmtpd-listen-on-configuration-filters listen-on-record) + (= 1 (length (opensmtpd-listen-on-configuration-filters + listen-on-record)))) + (car (opensmtpd-listen-on-configuration-filters listen-on-record)) + (opensmtpd-listen-on-configuration-filters listen-on-record))) + list-of-listen-on-records)) + listen-on-socket-filters))) + +(define (flatten . lst) + "Return a list that recursively concatenates all sub-lists of LST." + (define (flatten1 head out) + (if (list? head) + (fold-right flatten1 out head) + (cons head out))) + (fold-right flatten1 '() lst)) + +;; This function takes in a record, or list, or anything, and returns +;; a list of s assuming the thing you passed into it had +;; any s. +;; +;; is object record? call func on it's fieldnames +;; is object list? loop through it's fieldnames calling func on it's records +;; is object #f or string? or '()? -> #f +(define (get-opensmtpd-tables value) + (delete-duplicates + (remove boolean? (flatten ;; turn (list '(1) '(2 '(3))) -> '(1 2 3) + (cond ((opensmtpd-table-configuration? value) + value) + ((record? value) + (let* ([record-type (record-type-descriptor value)] + [list-of-record-fieldnames (record-type-fields record-type)]) + (map (lambda (fieldname) + (get-opensmtpd-tables ((record-accessor record-type fieldname) value))) + list-of-record-fieldnames))) + ((and (list? value) (not (null? value))) + (map get-opensmtpd-tables value)) + (else #f)))))) + +(define (opensmtpd-configuration-fieldname->string record fieldname-accessor record->string) + (if (fieldname-accessor record) + (begin + (string-append + (list-of-records->string (fieldname-accessor record) record->string) "\n")) + "")) + +(define (list-of-records->string list-of-records record->string) + (string-append + (cond [(not (list? list-of-records)) + (record->string list-of-records)] + [else + (let loop ([list list-of-records]) + (if (null? list) + "" + (string-append + (record->string (car list)) + (loop (cdr list)))))]))) + + +;; FIXME/TODO should I use format here srfi-28 ? +;; web.scm nginx does a (format #f "string" "another string") +;; this could be a list like (list (file-append opensmtpd-dkimsign "/libexec/filter") "-d gnucode.me -s /path/to/selector.cert") +;; Then opensmtpd-configuration->mixed-text-file could be rewritten to be something like +;; (mixed-text-file (eval `(string-append (opensmtpd-configuration-fieldname->string ...)) (gnu services mail))) +(define (opensmtpd-configuration->mixed-text-file record) + ;; should I use this named let, or should I give this a name, or not use it at all... + ;; eg: (write-all-fieldnames (list (cons fieldname fieldname->string) (cons fieldname2 fieldname->string))) + ;; (let loop ([list (list (cons opensmtpd-configuration-includes (lambda (string) + ;; (string-append + ;; "include \"" string "\"\n"))) + ;; (cons opensmtpd-configuration-smtp opensmtpd-smtp->string) + ;; (cons opensmtpd-configuration-srs opensmtpd-srs->string))]) + ;; (if (null? list) + ;; "" + ;; (string-append (opensmtpd-configuration-fieldname->string record + ;; (caar list) + ;; (cdar list)) + ;; (loop (cdr list))))) + + (mixed-text-file "opensmtpd.conf" + (string-append + ;; write out the includes + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-includes + opensmtpd-configuration-listen->string) + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-bounce + (lambda (%bounce) + (if %bounce + (list-of-strings->string %bounce) + ""))) + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-smtp + opensmtpd-smtp-configuration->string) + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-srs + opensmtpd-configuration-srs->string) + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-queue + opensmtpd-configuration-queue->string) + ;; write out the mta-max-deferred + (opensmtpd-configuration-fieldname->string + record opensmtpd-configuration-mta-max-deferred + (lambda (var) + (string-append "mta max-deferred " + (number->string (opensmtpd-configuration-mta-max-deferred record)) "\n"))) + ;;write out all the tables + (opensmtpd-configuration-fieldname->string record get-opensmtpd-tables opensmtpd-table-configuration->string) + ;; TODO should I change the below line of code into these two lines of code? + ;;(opensmtpd-configuration-fieldname->string record get-opensmtpd-filters-and-filter-phases opensmtpd-filter-and-filter-phase->string) + ;;(opensmtpd-configuration-fieldname->string record get-opensmtpd-filter-chains opensmtpd-filter-chain->string) + ;; write out all the filters + (opensmtpd-filters->string (get-opensmtpd-filters record)) + ;; write out all the cas + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-cas opensmtpd-ca-configuration->string) + ;; write out all the pkis + (opensmtpd-configuration-fieldname->string record get-opensmtpd-pki-configurations opensmtpd-pki-configuration->string) + ;; write all of the listen-on-records + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-listen-ons + opensmtpd-listen-on-configuration->string) + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-listen-on-socket + opensmtpd-listen-on-socket-configuration->string) + ;; write all the actions + (opensmtpd-configuration-fieldname->string record get-opensmtpd-actions + opensmtpd-action->string) + ;; write all of the matches + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-matches opensmtpd-match-configuration->string)))) + (define %default-opensmtpd-config-file (plain-file "smtpd.conf" " -- 2.36.1 From debbugs-submit-bounces@debbugs.gnu.org Tue Jul 05 17:36:34 2022 Received: (at 56046) by debbugs.gnu.org; 5 Jul 2022 21:36:34 +0000 Received: from localhost ([127.0.0.1]:51870 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o8qDh-0003Wd-TT for submit@debbugs.gnu.org; Tue, 05 Jul 2022 17:36:34 -0400 Received: from mx1.dismail.de ([78.46.223.134]:6762) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o8qDf-0003WN-Fp for 56046@debbugs.gnu.org; Tue, 05 Jul 2022 17:36:32 -0400 Received: from mx1.dismail.de (localhost [127.0.0.1]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 1a3b5552 for <56046@debbugs.gnu.org>; Tue, 5 Jul 2022 23:36:24 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; d=dismail.de; h= mime-version:date:content-type:from:message-id:subject:to; s= 20190914; bh=lRgguFIapdZWSDVvYsk7Dh7yc9nI0PBXXUf7kIkvqNg=; b=Nqo rlLo3+6H+rEinvWOEylJWlCMAJNfxPJFxDW0gvz60proHtY70R8EVXYGA2S9vIXI 0sG+9TqQpp3AHgLR/ozT2jUE1O/+EXRodoxHbZu/St23pjGm/YRyW0DuS9qmxTc3 02FC5SQE16/zdtgNTBAcS7dzuZHaAf6k6TNiKxPAgxSpkAhB0Uj9gr3u2O23TxQe dKdWDEehKmZez5Pg79oO56GvNSKcfD/oAhJbKtXQz/aLPRkfTWco8puZ1gD2kfkX UsFYI874AuLIIytDgiKy5OKgfNYkWHCYWMKONmZz5K/ymISuijQYk6F4Rk1YxjoO 7keleRb1dL36YFX/NEQ== Received: from smtp1.dismail.de ( [10.240.26.11]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 809c3a31 for <56046@debbugs.gnu.org>; Tue, 5 Jul 2022 23:36:23 +0200 (CEST) Received: from smtp1.dismail.de (localhost [127.0.0.1]) by smtp1.dismail.de (OpenSMTPD) with ESMTP id 68f28a72 for <56046@debbugs.gnu.org>; Tue, 5 Jul 2022 23:36:23 +0200 (CEST) Received: by dismail.de (OpenSMTPD) with ESMTPSA id fb8412e8 (TLSv1.3:TLS_AES_256_GCM_SHA384:256:NO) for <56046@debbugs.gnu.org>; Tue, 5 Jul 2022 23:36:23 +0200 (CEST) MIME-Version: 1.0 Date: Tue, 05 Jul 2022 21:36:22 +0000 Content-Type: multipart/alternative; boundary="--=_RainLoop_590_926031372.1657056982" X-Mailer: RainLoop/1.16.0a From: jbranso@dismail.de Message-ID: <1bace22a7dbd8668b9a774b0f975fa53@dismail.de> Subject: Re: [PATCH] services: mail: add opensmtpd records to enhance opensmtpd-configuration To: 56046@debbugs.gnu.org X-Spam-Score: -0.7 (/) X-Debbugs-Envelope-To: 56046 X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.7 (-) ----=_RainLoop_590_926031372.1657056982 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Here is a video link that may help the first reviewer: https://video.hardlimit.com/w/ixE7Tc1pCqpP3BgriRbYA5 (https://video.hardl= imit.com/w/ixE7Tc1pCqpP3BgriRbYA5) The video walks you through how to play with an example configuration in = emacs-geiser. Thanks, Joshua ----=_RainLoop_590_926031372.1657056982 Content-Type: text/html; charset="utf-8" Content-Transfer-Encoding: quoted-printable
<= br>
Here is a video link that may help the firs= t reviewer:

https://video.hardlimit.com/w/ixE7Tc1pCqpP3BgriRbYA5

The video walks you through how to play with an example configurati= on in emacs-geiser.

Thanks,

Joshua
----=_RainLoop_590_926031372.1657056982-- From debbugs-submit-bounces@debbugs.gnu.org Wed Jul 06 00:28:05 2022 Received: (at 56046) by debbugs.gnu.org; 6 Jul 2022 04:28:06 +0000 Received: from localhost ([127.0.0.1]:52027 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o8wdr-00059i-JP for submit@debbugs.gnu.org; Wed, 06 Jul 2022 00:28:05 -0400 Received: from mail-ej1-f67.google.com ([209.85.218.67]:45709) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o8wdk-00059N-9k for 56046@debbugs.gnu.org; Wed, 06 Jul 2022 00:27:57 -0400 Received: by mail-ej1-f67.google.com with SMTP id h23so24894321ejj.12 for <56046@debbugs.gnu.org>; Tue, 05 Jul 2022 21:27:52 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=message-id:subject:from:to:date:in-reply-to:references:user-agent :mime-version:content-transfer-encoding; bh=gduwxLChwEnV1zfeROMWD2TTCf0U4GW0foAab/DvmkU=; b=oxmmwhTY0a52Gu7tfJuEwO5mSJtnLJ4fipIuDcoOBfOCZ/n6WGuxeyvh9toEhlEWuc fB8FLK6MOZkNVz/tRWpaAJf3/Z6GVOuX3PXCTmGuxXMvFaZdXRRoyPILnYLjG3eY1zgr DtAnRLzVfKCl5JRNNPz5+C72Bkh0xPxsiad/2r4RMTehN7I8ndKdHLNgoFCCRsf6ZFYP c31GoqHB6AwFVc2EbYSiAG/eCdyxjmuOqLJixoReNQktwl1gGxqfPk9xlX+czEibkLP1 ivXgGloWWsphUecQE1f7zJfHzfXMfPot8KByDl0waalylog3UL7S3ohlBxHUMPCK5OQr eZug== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:message-id:subject:from:to:date:in-reply-to :references:user-agent:mime-version:content-transfer-encoding; bh=gduwxLChwEnV1zfeROMWD2TTCf0U4GW0foAab/DvmkU=; b=sIIPPHdE/M+ksaUzSZ2tIFVu2bCGJHejBV/Z7omXpUqvmyIzRC7IZKeRqo6Kzl8KsT wqjchszdzRcfWpDpfEHc7muV4W4vH47MVGQFljhPJjfPUNZjzg6GrPbAAS65WqlMYxmk 1b+FjxUEAZxjRORBcIH/jEi3Q6SrMlp7BZyWaQSqFcn9ofyjndo89Dsz0WIkCFaIpTVo kRv85l8krtCBxI3OQgF+KrttCvdqxNUREDSmIPhdrNSPTnppQ+SmMC7OQ9/nQ3eU4KTw 8RwZaIrkolk4CeU1tgDjR8Mb0aroFLB7qZG5ohImwP6pksZKZ+gVIe9jtvm78D7LG5UD PPBw== X-Gm-Message-State: AJIora/kzBdTYFvEMNw7SSbV5H3akwnHOFAUZYYwJdNS2v0wQZ7YK5r6 H69eckl1egv4hJBUJ0nOjeE= X-Google-Smtp-Source: AGRyM1vL3MyLJNnnhI1sURiM+IfbomW3JmT0piu+RCjNOwtDjLGAw7xqvoiimcCn4wHvNiXDfnMGCg== X-Received: by 2002:a17:906:2da:b0:712:14b:62da with SMTP id 26-20020a17090602da00b00712014b62damr36743944ejk.351.1657081665857; Tue, 05 Jul 2022 21:27:45 -0700 (PDT) Received: from nijino.fritz.box (85-127-52-93.dsl.dynamic.surfer.at. [85.127.52.93]) by smtp.gmail.com with ESMTPSA id la21-20020a170907781500b0072aeda86ac3sm1358592ejc.149.2022.07.05.21.27.45 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 05 Jul 2022 21:27:45 -0700 (PDT) Message-ID: <756a905107d7783bab238091d668fddbc1e712ab.camel@gmail.com> Subject: Re: [PATCH] services: mail: add opensmtpd records to enhance opensmtpd-configuration. Version 2 From: Liliana Marie Prikler To: Joshua Branson , 56046@debbugs.gnu.org Date: Wed, 06 Jul 2022 06:27:44 +0200 In-Reply-To: <20220704211759.8314-1-jbranso@dismail.de> References: <20220704211759.8314-1-jbranso@dismail.de> Content-Type: text/plain; charset="UTF-8" User-Agent: Evolution 3.42.1 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Spam-Score: 0.0 (/) X-Debbugs-Envelope-To: 56046 X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) Am Montag, dem 04.07.2022 um 17:17 -0400 schrieb Joshua Branson: > Openmstpd-configuration may only be configured by a config-file.  > This > patch, enables one to configure opensmtpd by using some guile record > types (defined via define-record-type*). > > * gnu/services/mail.scm:  New records (opensmtpd-table- > configuration), > (opensmtpd-ca-configuration), > (opensmtpd-pki-configuration), > (opensmtpd-action-local-delivery-configuration), > (opensmtpd-maildir-configuration), > (opensmtpd-mda-configuration), > (opensmtpd-action-relay-configuration), > (opensmtpd-option-configuration), > (opensmtpd-filter-phase-configuration), > (opensmtpd-filter-configuration), > (opensmtpd-listen-on-configuration), > (opensmtpd-listen-on-socket-configuration), > (opensmtpd-match-configuration), > (opensmtpd-smtp-configuration), > (opensmtpd-srs-configuration), > (opensmtpd-queue-configuration), and > (opensmtpd-configuration). Not a ChangeLog. > New procedures: false?, is-value-right-type, add-comma-or-string, > file-exists?, list-of-procedures->string, string-in-list?, my- > sanitize, > opensmtpd-filter-chain?, throw-error-duplicate-option, > sanitize-list-of-options-for-match-configuration, sanitize-filters, > list-has-duplicates-or-non-filters?, > filter-phase-has-message-and-value?, > filter-phase-decision-lacks-proper-message?, > filter-phase-lacks-proper-value?, > filter-phase-has-incorrect-junk-or-bypass?, > filter-phase-junks-after-commit?, > list-of-unique-filter-or-filter-phase?, throw-error, > contains-duplicate?, list-of-type?, list-of-strings?, > list-of-unique-opensmtpd-option-configuration?, > list-of-opensmtpd-ca-configuration?, > list-of-opensmtpd-pki-configuration?, > list-of-opensmtpd-listen-on-configuration?, > list-of-unique-opensmtpd-match-configuration?, list-of-strings- > >string, > assoc-list? assoc-list, variable->string, > table-whose-data-are-assoc-list?, > table-whose-data-are-a-list-of-strings?, assoc-list->string, > opensmtpd-table-configuration->string, > opensmtpd-listen-on-configuration->string, > opensmtpd-listen-on-socket-configuration->string, > opensmtpd-action-relay-configuration->string, > opensmtpd-lmtp-configuration->string, > opensmtpd-mda-configuration->string, > opensmtpd-maildir-configuration->string, > opensmtpd-action-local-delivery-configuration->string, > opensmtpd-action->string, opensmtpd-option-configuration->string, > opensmtpd-match-configuration->string, > opensmtpd-ca-configuration->string, opensmtpd-pki-configuration- > >string, > generate-filter-chain-name, opensmtpd-filter-chain->string, > opensmtpd-filter-phase-configuration->string, opensmtpd-filters- > >string, > opensmtpd-configuration-listen->string, > opensmtpd-configuration-srs->string, > opensmtpd-smtp-configuration->string, > opensmtpd-configuration-queue->string, get-opensmtpd-actions, > get-opensmtpd-pki-configurations, get-opensmtpd-filters, flatten, > get-opensmtpd-tables, opensmtpd-configuration-fieldname->string, > list-of-records->string, opensmtpd-configuration->mixed-text-file. Neither is this. > * doc/guix.texi added documentation for the new records for > opensmtpd. Or this. > --- >  doc/guix.texi         | 1051 ++++++++++++++++++++- >  gnu/services/mail.scm | 2016 > ++++++++++++++++++++++++++++++++++++++++- >  2 files changed, 3056 insertions(+), 11 deletions(-) > > diff --git a/doc/guix.texi b/doc/guix.texi > index eda0956260..e8564240d1 100644 > --- a/doc/guix.texi > +++ b/doc/guix.texi > @@ -24849,14 +24849,59 @@ could instantiate a dovecot service like > this: >  @subsubheading OpenSMTPD Service >   >  @deffn {Scheme Variable} opensmtpd-service-type > -This is the type of the @uref{https://www.opensmtpd.org, OpenSMTPD} > -service, whose value should be an @code{opensmtpd-configuration} > object > -as in this example: > - > -@lisp > -(service opensmtpd-service-type > -         (opensmtpd-configuration > -           (config-file (local-file "./my-smtpd.conf")))) > +OpenSMTPD is an easy-to-use mail transfer agent (MTA). Its > configuration file is > +throughly documented in @code{man 5 smtpd.conf}. OpenSMTPD > @strong{listens} for incoming > +mail and @strong{matches} the mail to @strong{actions}. The > following records represent those > +stages: > + > +@multitable {aaaaaaaaa} > {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} I suggest using fractions here. > [...] > +This is a string of one of these options: > + > +@multitable {aaaaaaaaaaaaaaaaaaaa} > {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} Same here. Btw. I did not actually check all the doc in between, so I might be missing something. > +@multitable {aaaaaaaaaaaaaaaaaaaaaaaaa} > {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} Likewise. > +@multitable {aaaaaaaaaa} > {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} You get the drill. > [more doc with strange multitables] > diff --git a/gnu/services/mail.scm b/gnu/services/mail.scm > index d99743ac31..2a344e303e 100644 > --- a/gnu/services/mail.scm > +++ b/gnu/services/mail.scm > @@ -57,8 +57,143 @@ (define-module (gnu services mail) >              mailbox-configuration >              namespace-configuration >   > +            opensmtpd-table-configuration > +            opensmtpd-table-configuration? > +            opensmtpd-table-configuration-name > +            opensmtpd-table-configuration-file-db > +            opensmtpd-table-configuration-data > + > +            opensmtpd-ca-configuration > +            opensmtpd-ca-configuration? > +            opensmtpd-ca-configuration-name > +            opensmtpd-ca-configuration-file > + > +            opensmtpd-pki-configuration > +            opensmtpd-pki-configuration? > +            opensmtpd-pki-configuration-domain > +            opensmtpd-pki-configuration-cert > +            opensmtpd-pki-configuration-key > +            opensmtpd-pki-configuration-dhe > + > +            opensmtpd-action-local-delivery-configuration > +            opensmtpd-action-local-delivery-configuration? > +            opensmtpd-action-local-delivery-configuration-method > +            opensmtpd-action-local-delivery-configuration-alias > +            opensmtpd-action-local-delivery-configuration-ttl > +            opensmtpd-action-local-delivery-configuration-user > +            opensmtpd-action-local-delivery-configuration-userbase > +            opensmtpd-action-local-delivery-configuration-virtual > +            opensmtpd-action-local-delivery-configuration-wrapper > + > +            opensmtpd-maildir-configuration > +            opensmtpd-maildir-configuration? > +            opensmtpd-maildir-configuration-pathname > +            opensmtpd-maildir-configuration-junk > + > +            opensmtpd-mda-configuration > +            opensmtpd-mda-configuration-name > +            opensmtpd-mda-configuration-command > + > +            opensmtpd-action-relay-configuration > +            opensmtpd-action-relay-configuration? > +            opensmtpd-action-relay-configuration-backup > +            opensmtpd-action-relay-configuration-backup-mx > +            opensmtpd-action-relay-configuration-helo > +            opensmtpd-action-relay-configuration-domain > +            opensmtpd-action-relay-configuration-host > +            opensmtpd-action-relay-configuration-pki > +            opensmtpd-action-relay-configuration-srs > +            opensmtpd-action-relay-configuration-tls > +            opensmtpd-action-relay-configuration-auth > +            opensmtpd-action-relay-configuration-mail-from > +            opensmtpd-action-relay-configuration-src > + > +            opensmtpd-option-configuration > +            opensmtpd-option-configuration? > +            opensmtpd-option-configuration-option > +            opensmtpd-option-configuration-not > +            opensmtpd-option-configuration-regex > +            opensmtpd-option-configuration-data > + > +            opensmtpd-filter-phase-configuration > +            opensmtpd-filter-phase-configuration? > +            opensmtpd-filter-phase-configuration-name > +            opensmtpd-filter-phase-configuration-phase-name > +            opensmtpd-filter-phase-configuration-options > +            opensmtpd-filter-phase-configuration-decision > +            opensmtpd-filter-phase-configuration-message > +            opensmtpd-filter-phase-configuration-value > + > +            opensmtpd-filter-configuration > +            opensmtpd-filter-configuration? > +            opensmtpd-filter-configuration-name > +            opensmtpd-filter-configuration-proc > + > +            opensmtpd-listen-on-configuration > +            opensmtpd-listen-on-configuration? > +            opensmtpd-listen-on-configuration-interface > +            opensmtpd-listen-on-configuration-family > +            opensmtpd-listen-on-configuration-auth > +            opensmtpd-listen-on-configuration-auth-optional > +            opensmtpd-listen-on-configuration-filters > +            opensmtpd-listen-on-configuration-hostname > +            opensmtpd-listen-on-configuration-hostnames > +            opensmtpd-listen-on-configuration-mask-src > +            opensmtpd-listen-on-configuration-disable-dsn > +            opensmtpd-listen-on-configuration-pki > +            opensmtpd-listen-on-configuration-port > +            opensmtpd-listen-on-configuration-proxy-v2 > +            opensmtpd-listen-on-configuration-received-auth > +            opensmtpd-listen-on-configuration-senders > +            opensmtpd-listen-on-configuration-secure-connection > +            opensmtpd-listen-on-configuration-tag > + > +            opensmtpd-listen-on-socket-configuration > +            opensmtpd-listen-on-socket-configuration? > +            opensmtpd-listen-on-socket-configuration-filters > +            opensmtpd-listen-on-socket-configuration-mask-src > +            opensmtpd-listen-on-socket-configuration-tag > + > +            opensmtpd-match-configuration > +            opensmtpd-match-configuration? > +            opensmtpd-match-configuration-action > +            opensmtpd-match-configuration-options > + > +            opensmtpd-smtp-configuration > +            opensmtpd-smtp-configuration? > +            opensmtpd-smtp-configuration-ciphers > +            opensmtpd-smtp-configuration-limit-max-mails > +            opensmtpd-smtp-configuration-limit-max-rcpt > +            opensmtpd-smtp-configuration-max-message-size > +            opensmtpd-smtp-configuration-sub-addr-delim character > + > +            opensmtpd-srs-configuration > +            opensmtpd-srs-configuration? > +            opensmtpd-srs-configuration-key > +            opensmtpd-srs-configuration-backup-key > +            opensmtpd-srs-configuration-ttl-delay > + > +            opensmtpd-queue-configuration > +            opensmtpd-queue-configuration? > +            opensmtpd-queue-configuration-compression > +            opensmtpd-queue-configuration-encryption > +            opensmtpd-queue-configuration-ttl-delay > + >              opensmtpd-configuration >              opensmtpd-configuration? > +            opensmtpd-package > +            opensmtpd-config-file > +            opensmtpd-configuration-bounce > +            opensmtpd-configuration-listen-ons > +            opensmtpd-configuration-listen-on-socket > +            opensmtpd-configuration-includes > +            opensmtpd-configuration-matches > +            opensmtpd-configuration-mda-wrappers > +            opensmtpd-configuration-mta-max-deferred > +            opensmtpd-configuration-srs > +            opensmtpd-configuration-smtp > +            opensmtpd-configuration-queue > + >              opensmtpd-service-type >              %default-opensmtpd-config-file >   > @@ -1651,13 +1786,1888 @@ (define (generate-dovecot-documentation) >  ;;; OpenSMTPD. >  ;;; >   > +;; some fieldnames have a default value of #f, which is ok.  They > cannot have a value of #t. > +;; for example opensmtpd-table-configuration-data can be #f, BUT NOT > true. > +;; my/sanitize procedure tests values to see if they are of the > right kind. > +;; procedure false? is needed to allow fields like 'values' to be > blank, (empty), or #f BUT also > +;; have a value like a list of strings. > +(define (false? var) > +  (eq? #f var)) I'm pretty sure it'd be fine to use not in lieu of false?, even at the risk of matching nil. > +;; this procedure takes in a var and a list of procedures.  It loops > through list of procedures passing in var to each. > +;; if one procedure returns #t, the function returns true.  > Otherwise #f. > +;; TODO for fun rewrite this using map > +;; If I rewrote it in map, then it may help with sanitizing. > +;; eg: I could then potentially easily sanitize vars with lambda > procedures. > +(define (is-value-right-type? var list-of-procedures record > fieldname) > +  (if (null? list-of-procedures) > +        #f > +        (cond [(procedure? (car list-of-procedures)) > +               (if ((car list-of-procedures) var) > +                   #t > +                   (is-value-right-type? var (cdr list-of- > procedures) record fieldname))] > +              [(and (sanitize-configuration? (car list-of- > procedures)) > +                    (sanitize-configuration-error-if-proc-fails (car > list-of-procedures)) > +                    (if ((sanitize-configuration-proc (car list-of- > procedures)) var) > +                        #t > +                        (begin > +                          (apply string-append > +                                 (sanitize-configuration-error- > message (car list-of-procedures))) > +                          (throw 'bad! var))))] > +              [else (if ((sanitize-configuration-proc (car list-of- > procedures)) var) > +                        #t > +                        (is-value-right-type? var (cdr list-of- > procedures) record fieldname))]))) Don't we have field sanitizers already that make this obsolete? > +;; converts strings like this: > +;; "apple, ham, cherry" -> "apple, ham, or cherry" > +;; "pineapple" -> "pinneapple". > +;; "cheese, grapefruit, or jam" -> "cheese, grapefruit, or jam" > +(define (add-comma-or string) > +  (define last-comma-location (string-rindex string #\,)) > +  (if last-comma-location > +      (if (string-contains string ", or" last-comma-location) > +          string > +          (string-replace string ", or" last-comma-location > +                          (+ 1 last-comma-location))) > +      string)) > + > +;; I could test for read-ability of a file, but then I would have to > +;; test the program as root everytime instead of as a normal user... > +(define (file-exists? file) > +(if (string? file) > +    (access? file F_OK) > +    #f)) Is this not part of the Guile standard library? > +(define (list-of-procedures->string procedures) > +  (define string > +    (let loop ([procedures procedures]) > +      (if (null? procedures) > +          "" > +          (begin > +            (string-append > +             (cond [(eq? false? (car procedures)) > +                    "#f , "] > +                   [(eq? boolean? (car procedures)) > +                    "boolean, "] > +                   [(eq? string? (car procedures)) > +                    "string, "] > +                   [(eq? integer? (car procedures)) > +                    "integer, "] > +                   [(eq? list-of-strings? (car procedures)) > +                    "list of strings, "] > +                   [(eq? assoc-list? (car procedures)) > +                    "an association list, "] > +                   [(eq? opensmtpd-pki-configuration? (car > procedures)) > +                    "an record, "] > +                   [(eq? opensmtpd-table-configuration? (car > procedures)) > +                    "an record, "] > +                   [(eq? list-of-unique-opensmtpd-match- > configuration? (car procedures)) > +                    "a list of unique configuration> records, "] > +                   [(eq? table-whose-data-are-assoc-list? (car > procedures)) > +                    (string-append > +                    "an record whose > fieldname 'values' are an assoc-list \n" > +                    "(eg: (opensmtpd-table-configuration (name > \"table\") (data '(\"joshua\" . \"$encrypted$password\")))), ")] > +                   [(eq? file-exists? (car procedures)) > +                    "file, "] > +                   [else "has an incorrect value, "]) > +             (loop (cdr procedures))))))) > +  (add-comma-or (string-append (string-drop-right string 2) ".\n"))) Using a table, map and string-join might be wiser. If this is the only place add-comma-or is used, you can replace it by  (string-append (string-join (butlast strings) ",") ", or " (last strings)) where you only need to define butlast. > +;; TODO can I M-x raise-sexp (string=? string var) in this > procedure? and get rid of checking > +;; if the var is a string?  The previous string-in-list? had that > check. > +;; (string-in-list? '("hello" 5 "cat")) currently works.  If I M-x > raise-sexp (string=? string var) > +;; then it will no longer work. > +(define (string-in-list? string list) > +  (primitive-eval (cons 'or (map (lambda (var) (and (string? var) > (string=? string var))) list)))) Ever heard of member? > +(define (my/sanitize var record fieldname list-of-procedures) > +  (if (is-value-right-type? var list-of-procedures record fieldname) > +      var > +      (begin > +        (display (string-append "<" record "> fieldname: '" > fieldname "' is of type " > +                                (list-of-procedures->string list-of- > procedures) "\n")) > +        (throw 'bad! var)))) > + > +;; Some example opensmtpd-table-configurations: > +;; > +;;  (opensmtpd-table-configuration (name "root accounts") (data > '(("joshua" . "root@dismail.de") ("joshua" . > "postmaster@dismail.de")))) > +;;  (opensmtpd-table-configuration (name "root accounts") (data > (list "mysite.me" "your-site.com"))) > +;;  TODO should support have a > fieldname 'file'? > +;;  Or should I change name to name-or-file ? > +(define-record-type* > +  opensmtpd-table-configuration make-opensmtpd-table-configuration > +  opensmtpd-table-configuration? > +  this-record > +  (name opensmtpd-table-configuration-name ;; string > +        (default #f) > +        (sanitize (lambda (var) > +                    (my/sanitize var "opensmtpd-table-configuration" > "name" (list string?))))) > +  (file-db opensmtpd-table-configuration-file-db > +           (default #f) > +           (sanitize (lambda (var) > +                       (my/sanitize var "opensmtpd-table- > configuration" "file-db" > +                                    (list boolean?))))) > +  ;; FIXME support an aliasing table as described here: > +  ;; https://man.openbsd.org/table.5 > +  ;; One may have to use the record file for this.  I don't think > tables support a table like this: > +  ;; table "name" { joshua = > joshua@gnucode.me,joshua@gnu-hurd.com,joshua@propernaming.org, root = > root@gnucode.me } > +  ;; If values is an absolute filename, then it will use said > filename to house the table info. > +  ;; filename must be an absolute filename. > +  (data opensmtpd-table-configuration-data > +          (default #f) > +          (sanitize (lambda (var) > +                      (my/sanitize var "opensmtpd-table- > configuration" "values" > +                                   (list file-exists? list-of- > strings? assoc-list?))))) > +  ;; is a list of values or key values > +  ;; eg: (list "mysite.me" "your-site.com") > +  ;; eg: (list ("joshua" . "joshua@gnu.org") ("james" . > "james@gnu.org")) > +  ;; I am currently making these values be as assocation list of > strings only. > +  ;; FIXME should I allow a var like this? > +  ;; (list (cons "gnucode.me" 234.949.392.23)) > +  ;; can be of type: (quote list-of-strings) or (quote assoc-list) > +  ;; (opensmtpd-table-configuration-type record) returns the values' > type.  The user SHOULD NEVER set the type. > +  ;; TODO jpoiret: on irc reccomends that I just use an outside > function to determine fieldname 'values', type. > +  ;; it would be "simpler" and possibly easier for the next person > working on this code to understand what is happening. > +  (type opensmtpd-table-configuration-type > +        (default #f) > +        (thunked) > +        (sanitize (lambda (var) > +                    (cond [(opensmtpd-table-configuration-data this- > record) > +                           (if (list-of-strings? (opensmtpd-table- > configuration-data this-record)) > +                               (quote list-of-strings) > +                               (quote assoc-list))] Just a quick side note, we don't usually intermix [ and (. It's all (. > [skipping a bit of stuff, may check later...] > +(define-record-type* > +  opensmtpd-option-configuration make-opensmtpd-option-configuration > +  opensmtpd-option-configuration? > +  (option opensmtpd-option-configuration-option > +          (default #f) > +          (sanitize (lambda (var) > +                      (if (and (string? var) > +                               (or (string-in-list? var (list > "fcrdns" "rdns" > +                                                          "src" > "helo" > +                                                          "auth" > "mail-from" > +                                                          "rcpt-to" > +                                                          "for" > +                                                          "for any" > "for local" > +                                                          "for > domain" "for rcpt-to" > +                                                          "from any" > "from auth" > +                                                          "from > local" "from mail-from" > +                                                          "from > rdns" "from socket" > +                                                          "from src" > "auth" > +                                                          "helo" > "mail-from" > +                                                          "rcpt-to" > "tag" "tls" > +                                                          )))) > +                          var > +                          (begin > +                            (display (string-append " option-configuration> fieldname: 'option' is of type \n" > +                                                    "string.  The > string can be either 'fcrdns', \n" > +                                                    " 'rdns', 'src', > 'helo', 'auth', 'mail-from', or 'rcpt-to', \n" > +                                                    "'for', 'for > any', 'for local', 'for domain', 'for rcpt-to', \n" > +                                                    "'from any', > 'from auth', 'from local', 'from mail-from', 'from rdns', 'from > socket', \n" > +                                                    "'from src', > 'auth helo', 'mail-from', 'rcpt-to', 'tag', or 'tls' \n" > +                                                    )) > +                            (throw 'bad! var)))))) This is a little verbose for what it does. > +(define-record-type* What is a "listen-on"? > +(define-record-type* configuration> Again, could this just be ? >  (define-record-type* >    opensmtpd-configuration make-opensmtpd-configuration >    opensmtpd-configuration? > -  (package     opensmtpd-configuration-package > -               (default opensmtpd)) > +  (package opensmtpd-configuration-package > +           (default opensmtpd)) >    (config-file opensmtpd-configuration-config-file > -               (default %default-opensmtpd-config-file))) > +               (default #f)) > +  ;; FIXME/TODO should I include a admd authservid entry? > + > +  ;; TODO sanitize this properly with perhaps a configuration>. > +  (bounce opensmtpd-configuration-bounce > +          (default #f) > +          (sanitize (lambda (var) > +                      (my/sanitize var "opensmtpd-configuration" > "bounce" > +                                   (list false? list?))))) > +  (cas opensmtpd-configuration-cas > +       (default #f) > +       (sanitize (lambda (var) > +                   (my/sanitize var "opensmtpd-configuration" "cas" > +                                (list false? list-of-opensmtpd-ca- > configuration?))))) > +  ;; list of many records of type opensmtpd-listen-on-configuration > +  (listen-ons opensmtpd-configuration-listen-ons What does opensmtpd acutally listen on? > [...] Too much to check, too little time. Maybe return later. From debbugs-submit-bounces@debbugs.gnu.org Wed Jul 06 17:51:45 2022 Received: (at 56046) by debbugs.gnu.org; 6 Jul 2022 21:51:45 +0000 Received: from localhost ([127.0.0.1]:55313 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o9Cvp-00089w-5V for submit@debbugs.gnu.org; Wed, 06 Jul 2022 17:51:45 -0400 Received: from mx1.dismail.de ([78.46.223.134]:41044) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o9Cvk-00089Z-91 for 56046@debbugs.gnu.org; Wed, 06 Jul 2022 17:51:35 -0400 Received: from mx1.dismail.de (localhost [127.0.0.1]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 32b3ce11; Wed, 6 Jul 2022 23:51:21 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; d=dismail.de; h= mime-version:date:content-type:content-transfer-encoding:from :message-id:subject:to:in-reply-to:references; s=20190914; bh=M3 lAaEhkW86YkTagxi7uVUSv51d6g7TerXXrvKBSwCI=; b=iclNkuBRM/HOGMQfE/ icAjrYe+AI6t7hKmghf4R+cC1npyLHk84fC8cSS7uPGOP0LfDowl3Co+mcsVucye mq0VPYPUANl8NBVlJF8DlolCwzEJ7y/jKQgORYnxe6mE1SLvodTuZtlrg7g9XnXn sXiocHBxvLbrkXaB7ggAa8gdpQvnHGdc/STxGescxAP9rmNM9noH64Xqv+fwprty +H/GhryNdpKjm63XjMI93s+egY4QSbZM6OuwJnj0L6qrtkbX94uqYZtPkIR4ltuB N2B1gQHmPfHBGBOk9QGSiu2bp5xZ7JEB822fxsrRw+/TEpWhgmNLk7pPn7eZDzf8 CCgw== Received: from smtp1.dismail.de ( [10.240.26.11]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 655320fa; Wed, 6 Jul 2022 23:51:21 +0200 (CEST) Received: from smtp1.dismail.de (localhost [127.0.0.1]) by smtp1.dismail.de (OpenSMTPD) with ESMTP id e411c72a; Wed, 6 Jul 2022 23:51:21 +0200 (CEST) Received: by dismail.de (OpenSMTPD) with ESMTPSA id 097f58c9 (TLSv1.3:TLS_AES_256_GCM_SHA384:256:NO); Wed, 6 Jul 2022 23:51:20 +0200 (CEST) MIME-Version: 1.0 Date: Wed, 06 Jul 2022 21:51:19 +0000 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable X-Mailer: RainLoop/1.16.0a From: jbranso@dismail.de Message-ID: <6a272ff438ca4e2efc8e196c9160f857@dismail.de> Subject: Re: [PATCH] services: mail: add opensmtpd records to enhance opensmtpd-configuration. Version 2 To: "Liliana Marie Prikler" , 56046@debbugs.gnu.org In-Reply-To: <756a905107d7783bab238091d668fddbc1e712ab.camel@gmail.com> References: <756a905107d7783bab238091d668fddbc1e712ab.camel@gmail.com> <20220704211759.8314-1-jbranso@dismail.de> X-Spam-Score: -0.7 (/) X-Debbugs-Envelope-To: 56046 X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) July 6, 2022 12:27 AM, "Liliana Marie Prikler" wrote: > Am Montag, dem 04.07.2022 um 17:17 -0400 schrieb Joshua Branson: >=20 >>=20Openmstpd-configuration may only be configured by a config-file.=20 >>=20This >> patch, enables one to configure opensmtpd by using some guile record >> types (defined via define-record-type*). >>=20 >>=20* gnu/services/mail.scm: New records (opensmtpd-table- >> configuration), >> (opensmtpd-ca-configuration), >> (opensmtpd-pki-configuration), >> (opensmtpd-action-local-delivery-configuration), >> (opensmtpd-maildir-configuration), >> (opensmtpd-mda-configuration), >> (opensmtpd-action-relay-configuration), >> (opensmtpd-option-configuration), >> (opensmtpd-filter-phase-configuration), >> (opensmtpd-filter-configuration), >> (opensmtpd-listen-on-configuration), >> (opensmtpd-listen-on-socket-configuration), >> (opensmtpd-match-configuration), >> (opensmtpd-smtp-configuration), >> (opensmtpd-srs-configuration), >> (opensmtpd-queue-configuration), and >> (opensmtpd-configuration). >=20 >=20Not a ChangeLog. I'll actually go and learn how to do that thanks. >=20 >>=20New procedures: false?, is-value-right-type, add-comma-or-string, >> file-exists?, list-of-procedures->string, string-in-list?, my- >> sanitize, >> opensmtpd-filter-chain?, throw-error-duplicate-option, >> sanitize-list-of-options-for-match-configuration, sanitize-filters, >> list-has-duplicates-or-non-filters?, >> filter-phase-has-message-and-value?, >> filter-phase-decision-lacks-proper-message?, >> filter-phase-lacks-proper-value?, >> filter-phase-has-incorrect-junk-or-bypass?, >> filter-phase-junks-after-commit?, >> list-of-unique-filter-or-filter-phase?, throw-error, >> contains-duplicate?, list-of-type?, list-of-strings?, >> list-of-unique-opensmtpd-option-configuration?, >> list-of-opensmtpd-ca-configuration?, >> list-of-opensmtpd-pki-configuration?, >> list-of-opensmtpd-listen-on-configuration?, >> list-of-unique-opensmtpd-match-configuration?, list-of-strings- >> string, >> assoc-list? assoc-list, variable->string, >> table-whose-data-are-assoc-list?, >> table-whose-data-are-a-list-of-strings?, assoc-list->string, >> opensmtpd-table-configuration->string, >> opensmtpd-listen-on-configuration->string, >> opensmtpd-listen-on-socket-configuration->string, >> opensmtpd-action-relay-configuration->string, >> opensmtpd-lmtp-configuration->string, >> opensmtpd-mda-configuration->string, >> opensmtpd-maildir-configuration->string, >> opensmtpd-action-local-delivery-configuration->string, >> opensmtpd-action->string, opensmtpd-option-configuration->string, >> opensmtpd-match-configuration->string, >> opensmtpd-ca-configuration->string, opensmtpd-pki-configuration- >> string, >> generate-filter-chain-name, opensmtpd-filter-chain->string, >> opensmtpd-filter-phase-configuration->string, opensmtpd-filters- >> string, >> opensmtpd-configuration-listen->string, >> opensmtpd-configuration-srs->string, >> opensmtpd-smtp-configuration->string, >> opensmtpd-configuration-queue->string, get-opensmtpd-actions, >> get-opensmtpd-pki-configurations, get-opensmtpd-filters, flatten, >> get-opensmtpd-tables, opensmtpd-configuration-fieldname->string, >> list-of-records->string, opensmtpd-configuration->mixed-text-file. >=20 >=20Neither is this. >> * doc/guix.texi added documentation for the new records for >> opensmtpd. >=20 >=20Or this. >=20 >>=20--- >> doc/guix.texi | 1051 ++++++++++++++++++++- >> gnu/services/mail.scm | 2016 >> ++++++++++++++++++++++++++++++++++++++++- >> 2 files changed, 3056 insertions(+), 11 deletions(-) >>=20 >>=20diff --git a/doc/guix.texi b/doc/guix.texi >> index eda0956260..e8564240d1 100644 >> --- a/doc/guix.texi >> +++ b/doc/guix.texi >> @@ -24849,14 +24849,59 @@ could instantiate a dovecot service like >> this: >> @subsubheading OpenSMTPD Service >>=20 >>=20@deffn {Scheme Variable} opensmtpd-service-type >> -This is the type of the @uref{https://www.opensmtpd.org, OpenSMTPD} >> -service, whose value should be an @code{opensmtpd-configuration} >> object >> -as in this example: >> - >> -@lisp >> -(service opensmtpd-service-type >> - (opensmtpd-configuration >> - (config-file (local-file "./my-smtpd.conf")))) >> +OpenSMTPD is an easy-to-use mail transfer agent (MTA). Its >> configuration file is >> +throughly documented in @code{man 5 smtpd.conf}. OpenSMTPD >> @strong{listens} for incoming >> +mail and @strong{matches} the mail to @strong{actions}. The >> following records represent those >> +stages: >> + >> +@multitable {aaaaaaaaa} >> {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} >=20 >=20I suggest using fractions here. I'll take a look at the texinfo fractions bit. This was all generated=20 from=20an org-mode document. >=20 >>=20[...] >> +This is a string of one of these options: >> + >> +@multitable {aaaaaaaaaaaaaaaaaaaa} >> {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} >=20 >=20Same here. Btw. I did not actually check all the doc in between, so I > might be missing something. >> +@multitable {aaaaaaaaaaaaaaaaaaaaaaaaa} >> {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} >=20 >=20Likewise. >> +@multitable {aaaaaaaaaa} >> {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} >=20 >=20You get the drill. >=20 >>=20[more doc with strange multitables] >> diff --git a/gnu/services/mail.scm b/gnu/services/mail.scm >> index d99743ac31..2a344e303e 100644 >> --- a/gnu/services/mail.scm >> +++ b/gnu/services/mail.scm >> @@ -57,8 +57,143 @@ (define-module (gnu services mail) >> mailbox-configuration >> namespace-configuration >>=20 >>=20+ opensmtpd-table-configuration >> + opensmtpd-table-configuration? >> + opensmtpd-table-configuration-name >> + opensmtpd-table-configuration-file-db >> + opensmtpd-table-configuration-data >> + >> + opensmtpd-ca-configuration >> + opensmtpd-ca-configuration? >> + opensmtpd-ca-configuration-name >> + opensmtpd-ca-configuration-file >> + >> + opensmtpd-pki-configuration >> + opensmtpd-pki-configuration? >> + opensmtpd-pki-configuration-domain >> + opensmtpd-pki-configuration-cert >> + opensmtpd-pki-configuration-key >> + opensmtpd-pki-configuration-dhe >> + >> + opensmtpd-action-local-delivery-configuration >> + opensmtpd-action-local-delivery-configuration? >> + opensmtpd-action-local-delivery-configuration-method >> + opensmtpd-action-local-delivery-configuration-alias >> + opensmtpd-action-local-delivery-configuration-ttl >> + opensmtpd-action-local-delivery-configuration-user >> + opensmtpd-action-local-delivery-configuration-userbase >> + opensmtpd-action-local-delivery-configuration-virtual >> + opensmtpd-action-local-delivery-configuration-wrapper >> + >> + opensmtpd-maildir-configuration >> + opensmtpd-maildir-configuration? >> + opensmtpd-maildir-configuration-pathname >> + opensmtpd-maildir-configuration-junk >> + >> + opensmtpd-mda-configuration >> + opensmtpd-mda-configuration-name >> + opensmtpd-mda-configuration-command >> + >> + opensmtpd-action-relay-configuration >> + opensmtpd-action-relay-configuration? >> + opensmtpd-action-relay-configuration-backup >> + opensmtpd-action-relay-configuration-backup-mx >> + opensmtpd-action-relay-configuration-helo >> + opensmtpd-action-relay-configuration-domain >> + opensmtpd-action-relay-configuration-host >> + opensmtpd-action-relay-configuration-pki >> + opensmtpd-action-relay-configuration-srs >> + opensmtpd-action-relay-configuration-tls >> + opensmtpd-action-relay-configuration-auth >> + opensmtpd-action-relay-configuration-mail-from >> + opensmtpd-action-relay-configuration-src >> + >> +=20=20=20=20=20 opensmtpd-option-configuration >> + opensmtpd-option-configuration? >> + opensmtpd-option-configuration-option >> + opensmtpd-option-configuration-not >> + opensmtpd-option-configuration-regex >> + opensmtpd-option-configuration-data >> + >> + opensmtpd-filter-phase-configuration >> + opensmtpd-filter-phase-configuration? >> + opensmtpd-filter-phase-configuration-name >> + opensmtpd-filter-phase-configuration-phase-name >> + opensmtpd-filter-phase-configuration-options >> + opensmtpd-filter-phase-configuration-decision >> + opensmtpd-filter-phase-configuration-message >> + opensmtpd-filter-phase-configuration-value >> + >> + opensmtpd-filter-configuration >> + opensmtpd-filter-configuration? >> + opensmtpd-filter-configuration-name >> + opensmtpd-filter-configuration-proc >> + >> + opensmtpd-listen-on-configuration >> + opensmtpd-listen-on-configuration? >> + opensmtpd-listen-on-configuration-interface >> + opensmtpd-listen-on-configuration-family >> + opensmtpd-listen-on-configuration-auth >> + opensmtpd-listen-on-configuration-auth-optional >> + opensmtpd-listen-on-configuration-filters >> + opensmtpd-listen-on-configuration-hostname >> + opensmtpd-listen-on-configuration-hostnames >> + opensmtpd-listen-on-configuration-mask-src >> + opensmtpd-listen-on-configuration-disable-dsn >> + opensmtpd-listen-on-configuration-pki >> + opensmtpd-listen-on-configuration-port >> + opensmtpd-listen-on-configuration-proxy-v2 >> + opensmtpd-listen-on-configuration-received-auth >> + opensmtpd-listen-on-configuration-senders >> + opensmtpd-listen-on-configuration-secure-connection >> + opensmtpd-listen-on-configuration-tag >> + >> + opensmtpd-listen-on-socket-configuration >> + opensmtpd-listen-on-socket-configuration? >> + opensmtpd-listen-on-socket-configuration-filters >> + opensmtpd-listen-on-socket-configuration-mask-src >> + opensmtpd-listen-on-socket-configuration-tag >> + >> + opensmtpd-match-configuration >> + opensmtpd-match-configuration? >> + opensmtpd-match-configuration-action >> + opensmtpd-match-configuration-options >> + >> + opensmtpd-smtp-configuration >> + opensmtpd-smtp-configuration? >> + opensmtpd-smtp-configuration-ciphers >> + opensmtpd-smtp-configuration-limit-max-mails >> + opensmtpd-smtp-configuration-limit-max-rcpt >> + opensmtpd-smtp-configuration-max-message-size >> + opensmtpd-smtp-configuration-sub-addr-delim character >> + >> + opensmtpd-srs-configuration >> + opensmtpd-srs-configuration? >> + opensmtpd-srs-configuration-key >> + opensmtpd-srs-configuration-backup-key >> + opensmtpd-srs-configuration-ttl-delay >> + >> + opensmtpd-queue-configuration >> + opensmtpd-queue-configuration? >> + opensmtpd-queue-configuration-compression >> + opensmtpd-queue-configuration-encryption >> + opensmtpd-queue-configuration-ttl-delay >> + >> opensmtpd-configuration >> opensmtpd-configuration? >> + opensmtpd-package >> + opensmtpd-config-file >> + opensmtpd-configuration-bounce >> + opensmtpd-configuration-listen-ons >> + opensmtpd-configuration-listen-on-socket >> + opensmtpd-configuration-includes >> + opensmtpd-configuration-matches >> + opensmtpd-configuration-mda-wrappers >> + opensmtpd-configuration-mta-max-deferred >> + opensmtpd-configuration-srs >> + opensmtpd-configuration-smtp >> + opensmtpd-configuration-queue >> + >> opensmtpd-service-type >> %default-opensmtpd-config-file >>=20 >>=20@@ -1651,13 +1786,1888 @@ (define (generate-dovecot-documentation) >> ;;; OpenSMTPD. >> ;;; >>=20 >>=20+;; some fieldnames have a default value of #f, which is ok. They >> cannot have a value of #t. >> +;; for example opensmtpd-table-configuration-data can be #f, BUT NOT >> true. >> +;; my/sanitize procedure tests values to see if they are of the >> right kind. >> +;; procedure false? is needed to allow fields like 'values' to be >> blank, (empty), or #f BUT also >> +;; have a value like a list of strings. >> +(define (false? var) >> + (eq? #f var)) >=20 >=20I'm pretty sure it'd be fine to use not in lieu of false?, even at th= e > risk of matching nil. Sounds ok. I may need to double check that there are no places that have nil values. >=20 >>=20+;; this procedure takes in a var and a list of procedures. It loop= s >> through list of procedures passing in var to each. >> +;; if one procedure returns #t, the function returns true.=20 >>=20Otherwise #f. >> +;; TODO for fun rewrite this using map >> +;; If I rewrote it in map, then it may help with sanitizing. >> +;; eg: I could then potentially easily sanitize vars with lambda >> procedures. >> +(define (is-value-right-type? var list-of-procedures record >> fieldname) >> + (if (null? list-of-procedures) >> + #f >> + (cond [(procedure? (car list-of-procedures)) >> + (if ((car list-of-procedures) var) >> + #t >> + (is-value-right-type? var (cdr list-of- >> procedures) record fieldname))] >> + [(and (sanitize-configuration? (car list-of- >> procedures)) >> + (sanitize-configuration-error-if-proc-fails (car >> list-of-procedures)) >> + (if ((sanitize-configuration-proc (car list-of- >> procedures)) var) >> + #t >> + (begin >> + (apply string-append >> + (sanitize-configuration-error- >> message (car list-of-procedures))) >> + (throw 'bad! var))))] >> + [else (if ((sanitize-configuration-proc (car list-of- >> procedures)) var) >> + #t >> + (is-value-right-type? var (cdr list-of- >> procedures) record fieldname))]))) >=20 >=20Don't we have field sanitizers already that make this obsolete? >=20 >>=20+;; converts strings like this: >> +;; "apple, ham, cherry" -> "apple, ham, or cherry" >> +;; "pineapple" -> "pinneapple". >> +;; "cheese, grapefruit, or jam" -> "cheese, grapefruit, or jam" >> +(define (add-comma-or string) >> + (define last-comma-location (string-rindex string #\,)) >> + (if last-comma-location >> + (if (string-contains string ", or" last-comma-location) >> + string >> + (string-replace string ", or" last-comma-location >> + (+ 1 last-comma-location))) >> + string)) >> + >> +;; I could test for read-ability of a file, but then I would have to >> +;; test the program as root everytime instead of as a normal user... >> +(define (file-exists? file) >> +(if (string? file) >> + (access? file F_OK) >> + #f)) >=20 >=20Is this not part of the Guile standard library? I do not believe that guile has a file-exists? thunk.=20=20 I=20could use (access? file F_OK) every time, but I think file-exists? is easier to use. My two cents. >=20 >>=20+(define (list-of-procedures->string procedures) >> + (define string >> + (let loop ([procedures procedures]) >> + (if (null? procedures) >> + "" >> + (begin >> + (string-append >> + (cond [(eq? false? (car procedures)) >> + "#f , "] >> + [(eq? boolean? (car procedures)) >> + "boolean, "] >> + [(eq? string? (car procedures)) >> +=20=20=20=20=20=20=20=20=20=20 "string, "] >> + [(eq? integer? (car procedures)) >> + "integer, "] >> + [(eq? list-of-strings? (car procedures)) >> + "list of strings, "] >> + [(eq? assoc-list? (car procedures)) >> + "an association list, "] >> + [(eq? opensmtpd-pki-configuration? (car >> procedures)) >> + "an record, "] >> + [(eq? opensmtpd-table-configuration? (car >> procedures)) >> + "an record, "] >> + [(eq? list-of-unique-opensmtpd-match- >> configuration? (car procedures)) >> + "a list of unique > configuration> records, "] >> + [(eq? table-whose-data-are-assoc-list? (car >> procedures)) >> + (string-append >> + "an record whose >> fieldname 'values' are an assoc-list \n" >> + "(eg: (opensmtpd-table-configuration (name >> \"table\") (data '(\"joshua\" . \"$encrypted$password\")))), ")] >> + [(eq? file-exists? (car procedures)) >> + "file, "] >> + [else "has an incorrect value, "]) >> + (loop (cdr procedures))))))) >> + (add-comma-or (string-append (string-drop-right string 2) ".\n"))) >=20 >=20Using a table, map and string-join might be wiser. If this is the onl= y > place add-comma-or is used, you can replace it by=20 >=20(string-append > (string-join (butlast strings) ",") > ", or " (last strings)) > where you only need to define butlast. I'll take a look. >=20 >>=20+;; TODO can I M-x raise-sexp (string=3D? string var) in this >> procedure? and get rid of checking >> +;; if the var is a string? The previous string-in-list? had that >> check. >> +;; (string-in-list? '("hello" 5 "cat")) currently works. If I M-x >> raise-sexp (string=3D? string var) >> +;; then it will no longer work. >> +(define (string-in-list? string list) >> + (primitive-eval (cons 'or (map (lambda (var) (and (string? var) >> (string=3D? string var))) list)))) >=20 >=20Ever heard of member? Will use it. >=20 >>=20+(define (my/sanitize var record fieldname list-of-procedures) >> + (if (is-value-right-type? var list-of-procedures record fieldname) >> + var >> + (begin >> + (display (string-append "<" record "> fieldname: '" >> fieldname "' is of type " >> + (list-of-procedures->string list-of- >> procedures) "\n")) >> + (throw 'bad! var)))) >> + >> +;; Some example opensmtpd-table-configurations: >> +;; >> +;; (opensmtpd-table-configuration (name "root accounts") (data >> '(("joshua" . "root@dismail.de") ("joshua" . >> "postmaster@dismail.de")))) >> +;; (opensmtpd-table-configuration (name "root accounts") (data >> (list "mysite.me" "your-site.com"))) >> +;; TODO should support have a >> fieldname 'file'? >> +;; Or should I change name to name-or-file ? >> +(define-record-type* >> + opensmtpd-table-configuration make-opensmtpd-table-configuration >> + opensmtpd-table-configuration? >> + this-record >> + (name opensmtpd-table-configuration-name ;; string >> + (default #f) >> + (sanitize (lambda (var) >> + (my/sanitize var "opensmtpd-table-configuration" >> "name" (list string?))))) >> + (file-db opensmtpd-table-configuration-file-db >> + (default #f) >> + (sanitize (lambda (var) >> + (my/sanitize var "opensmtpd-table- >> configuration" "file-db" >> + (list boolean?))))) >> + ;; FIXME support an aliasing table as described here: >> + ;; https://man.openbsd.org/table.5 >> + ;; One may have to use the record file for this. I don't think >> tables support a table like this: >> + ;; table "name" { joshua =3D >> joshua@gnucode.me,joshua@gnu-hurd.com,joshua@propernaming.org, root = =3D >> root@gnucode.me } >> + ;; If values is an absolute filename, then it will use said >> filename to house the table info. >> + ;; filename must be an absolute filename. >> + (data opensmtpd-table-configuration-data >> + (default #f) >> + (sanitize (lambda (var) >> + (my/sanitize var "opensmtpd-table- >> configuration" "values" >> + (list file-exists? list-of- >> strings? assoc-list?))))) >> + ;; is a list of values or key values >> + ;; eg: (list "mysite.me" "your-site.com") >> + ;; eg: (list ("joshua" . "joshua@gnu.org") ("james" . >> "james@gnu.org")) >> + ;; I am currently making these values be as assocation list of >> strings only. >> + ;; FIXME should I allow a var like this? >> + ;; (list (cons "gnucode.me" 234.949.392.23)) >> + ;; can be of type: (quote list-of-strings) or (quote assoc-list) >> + ;; (opensmtpd-table-configuration-type record) returns the values' >> type. The user SHOULD NEVER set the type. >> + ;; TODO jpoiret: on irc reccomends that I just use an outside >> function to determine fieldname 'values', type. >> + ;; it would be "simpler" and possibly easier for the next person >> working on this code to understand what is happening. >> + (type opensmtpd-table-configuration-type >> + (default #f) >> + (thunked) >> + (sanitize (lambda (var) >> + (cond [(opensmtpd-table-configuration-data this- >> record) >> + (if (list-of-strings? (opensmtpd-table- >> configuration-data this-record)) >> + (quote list-of-strings) >> + (quote assoc-list))] >=20 >=20Just a quick side note, we don't usually intermix [ and (. It's all (= . Yeah I saw that in the coding style. I'll fix it. >=20 >>=20[skipping a bit of stuff, may check later...] >> +(define-record-type* >> + opensmtpd-option-configuration make-opensmtpd-option-configuration >> + opensmtpd-option-configuration? >> + (option opensmtpd-option-configuration-option >> + (default #f) >> + (sanitize (lambda (var) >> + (if (and (string? var) >> + (or (string-in-list? var (list >> "fcrdns" "rdns" >> + "src" >> "helo" >> + "auth" >> "mail-from" >> + "rcpt-to" >> + "for" >> + "for any" >> "for local" >> + "for >> domain" "for rcpt-to" >> + "from any" >> "from auth" >> + "from >> local" "from mail-from" >> + "from >> rdns" "from socket" >> + "from src" >> "auth" >> + "helo" >> "mail-from" >> + "rcpt-to" >> "tag" "tls" >> + )))) >> + var >> + (begin >> + (display (string-append "> option-configuration> fieldname: 'option' is of type \n" >> + "string. The >> string can be either 'fcrdns', \n" >> + " 'rdns', 'src', >> 'helo', 'auth', 'mail-from', or 'rcpt-to', \n" >> + "'for', 'for >> any', 'for local', 'for domain', 'for rcpt-to', \n" >> + "'from any', >> 'from auth', 'from local', 'from mail-from', 'from rdns', 'from >> socket', \n" >> + "'from src', >> 'auth helo', 'mail-from', 'rcpt-to', 'tag', or 'tls' \n" >> + )) >> + (throw 'bad! var)))))) >=20 >=20This is a little verbose for what it does. Fair I suppose. I'll see if I can shorten it. >=20 >>=20+(define-record-type* >=20 >=20What is a "listen-on"? "listen-on" refers to the "listen on" in smtpd.conf: https://man.openbsd.org/smtpd.conf lan_addr =3D "192.168.0.1" listen on $lan_addr listen on $lan_addr tls auth >> +(define-record-type* > configuration> >=20 >=20Again, could this just be ? I would prefer to have two data types for "listen on" and "listen on socket". "listen on socket" only supports 3 options, where "listen on" supports 19.=20=20 https://man.openbsd.org/smtpd.conf ;;valid=20record (openstmpd-listen-on-configuration (tag "port-48") (port 48)) ;;invalid record and will result in an error as it should. (openstmpd-listen-on-socket-configuration (tag "port-48") (port 48)) >=20 >>=20(define-record-type* >> opensmtpd-configuration make-opensmtpd-configuration >> opensmtpd-configuration? >> - (package opensmtpd-configuration-package >> - (default opensmtpd)) >> + (package opensmtpd-configuration-package >> + (default opensmtpd)) >> (config-file opensmtpd-configuration-config-file >> - (default %default-opensmtpd-config-file))) >> + (default #f)) >> + ;; FIXME/TODO should I include a admd authservid entry? >> + >> + ;; TODO sanitize this properly with perhaps a > configuration>. >> + (bounce opensmtpd-configuration-bounce >> + (default #f) >> + (sanitize (lambda (var) >> + (my/sanitize var "opensmtpd-configuration" >> "bounce" >> + (list false? list?))))) >> + (cas opensmtpd-configuration-cas >> + (default #f) >> + (sanitize (lambda (var) >> + (my/sanitize var "opensmtpd-configuration" "cas" >> + (list false? list-of-opensmtpd-ca- >> configuration?))))) >> + ;; list of many records of type opensmtpd-listen-on-configuration >> + (listen-ons opensmtpd-configuration-listen-ons >=20 >=20What does opensmtpd acutally listen on? >From the documentation: listen on interface [family] [options] Listen on the interface for incoming connections, using the same synt= ax as ifconfig(8). The interface parameter may also be an interface group= , an IP address, or a domain name. Listening can optionally be restricted= to a specific address family, which can be either inet4 or inet6.=20 listen=20on socket [options] Listen for incoming SMTP connections on the Unix domain socket /var/r= un/smtpd.sock. This is done by default, even if the directive is absent.= =20 >=20 >> [...] >=20 >=20Too much to check, too little time. Maybe return later. To summarize the tasks that you have given me are: 1) Write a proper changelog. 2) define "string-in-list?" with member?=20 =20 Are there other procedures that could use this? 3) replace [] with () 4) Shorten the sanitize procedure for opensmtpd-option-configuration Thanks for reviewing! Joshua From debbugs-submit-bounces@debbugs.gnu.org Thu Jul 07 00:25:47 2022 Received: (at 56046) by debbugs.gnu.org; 7 Jul 2022 04:25:47 +0000 Received: from localhost ([127.0.0.1]:55465 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o9J5F-0003Fp-CY for submit@debbugs.gnu.org; Thu, 07 Jul 2022 00:25:47 -0400 Received: from mail-ej1-f68.google.com ([209.85.218.68]:41598) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o9J5A-0003FY-Kd for 56046@debbugs.gnu.org; Thu, 07 Jul 2022 00:25:44 -0400 Received: by mail-ej1-f68.google.com with SMTP id u12so30289872eja.8 for <56046@debbugs.gnu.org>; Wed, 06 Jul 2022 21:25:40 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=message-id:subject:from:to:date:in-reply-to:references:user-agent :mime-version:content-transfer-encoding; bh=mER72DH/rkHh76cSAamAG9HOQPGoN8SNKo3lzZdOLg4=; b=pdY8Ab7igE4LZnoFwKg2/TdzV5uMXtDe9ZrCgIp6t1COzTqhXriE6MTpNR251yOWWs UscPm2nQsNqgvCsFrnlSUogF440rGq21APas+0IayBSv5SfpQ6y1lwH+K3ezKUPq1JAb ueKenYwqlRBBEYl7lWzsq7L26dUQduRHQx8Tv42gmVv/FVbVRTYGIPJCmcq+y5BK6pkz 8dF9i1IPSZrLTrPHyo3Tcw6umMGLuKkKK/wqPuldQJ8HvgAmRvXGtT7rle+hupjsMhiT +wP1oKXqchCgEWxVjpOTpd/LIbMJQM2EYtLpCFrnLjJ2NsWkh/TLxNuU5S02T/28CkwF eL7A== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:message-id:subject:from:to:date:in-reply-to :references:user-agent:mime-version:content-transfer-encoding; bh=mER72DH/rkHh76cSAamAG9HOQPGoN8SNKo3lzZdOLg4=; b=Fhs1HRPIeX+bs2rtYUrIPf8JnVgB7gQZX7UeZoEFdQn8vhxVzPIcal/GkDp5na9ash sNKkpIgjWOTtVYSD1J/2jIuprdesBRrHQ67BJ6Hml6giu9/YZiQd0pP7N/r4dyZEjaeS 2NCvUXs5ypTEepPB8NYniSEKHTzDVC4v/FMUo69NL03I+lh4yMiySgReefk9hkTax8EP 3ouvfBf/WOhPvj0c2F26amZNOPs82YALkwWIZOYrPmcvudFHvEd3aFA+HBGZ/5ehl1qQ pgePw50TfkCB6P+eGYPWDrFzZdu80Jn4dgvUWfZVySh2LWDC1uPXZ6pbHw651IrBIbEF 6GJA== X-Gm-Message-State: AJIora/Hg1V3wkbige057gS9ExpwMw6xgtnoxsvH9wKHpBTdSSEcfKAF r/Jzwy0pVGctA6s2BtWusy8= X-Google-Smtp-Source: AGRyM1ukYaPxrduJXVUPMT0Eo16mVSf1Br8n/pHqkPJHsn1o2U82i9YlGDLpGnrf/3cejpAY425dKw== X-Received: by 2002:a17:907:1693:b0:726:4322:c330 with SMTP id hc19-20020a170907169300b007264322c330mr43209664ejc.9.1657167934559; Wed, 06 Jul 2022 21:25:34 -0700 (PDT) Received: from nijino.fritz.box (85-127-52-93.dsl.dynamic.surfer.at. [85.127.52.93]) by smtp.gmail.com with ESMTPSA id jp1-20020a170906f74100b006fe0abb00f0sm18184620ejb.209.2022.07.06.21.25.33 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 06 Jul 2022 21:25:34 -0700 (PDT) Message-ID: Subject: Re: [PATCH] services: mail: add opensmtpd records to enhance opensmtpd-configuration. Version 2 From: Liliana Marie Prikler To: jbranso@dismail.de, 56046@debbugs.gnu.org Date: Thu, 07 Jul 2022 06:25:32 +0200 In-Reply-To: <6a272ff438ca4e2efc8e196c9160f857@dismail.de> References: <756a905107d7783bab238091d668fddbc1e712ab.camel@gmail.com> <20220704211759.8314-1-jbranso@dismail.de> <6a272ff438ca4e2efc8e196c9160f857@dismail.de> Content-Type: text/plain; charset="UTF-8" User-Agent: Evolution 3.42.1 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Spam-Score: -0.0 (/) X-Debbugs-Envelope-To: 56046 X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) Am Mittwoch, dem 06.07.2022 um 21:51 +0000 schrieb jbranso@dismail.de: > I do not believe that guile has a file-exists? thunk.  > I could use (access? file F_OK) every time, but I think > file-exists? is easier to use.  My two cents. scheme@(guile-user)> file-exists? $1 = # > > > What is a "listen-on"? > > "listen-on" refers to the "listen on" in smtpd.conf: > https://man.openbsd.org/smtpd.conf > > lan_addr = "192.168.0.1" > listen on $lan_addr > listen on $lan_addr tls auth Okay, but what would you call that? An address maybe? > > > +(define-record-type* > > configuration> > > > > Again, could this just be ? > > I would prefer to have two data types for "listen on" and > "listen on socket".  "listen on socket" only supports 3 options, > where "listen on" supports 19. I am not questioning whether it makes sense to add a configuration record – it probably does – but whether you're using the best name for that record. We are not Java programmers here, a little abstraction goes a long way. > From the documentation: > > listen on interface [family] [options] >     Listen on the interface for incoming connections, using the same > syntax as ifconfig(8). The interface parameter may also be an > interface group, an IP address, or a domain name. Listening can > optionally be restricted to a specific address family, which can be > either inet4 or inet6. > > listen on socket [options] >     Listen for incoming SMTP connections on the Unix domain socket > /var/run/smtpd.sock. This is done by default, even if the directive > is absent. So you can either have an opensmtp-interface (with family and a bunch of options) or an opensmtp-socket (with a bunch of options). Sounds like a much nicer ontology, doesn't it? > > > [...] > > > > Too much to check, too little time. Maybe return later. > > To summarize the tasks that you have given me are: > > 1) Write a proper changelog. > 2) define "string-in-list?" with member? >    Are there other procedures that could use this? > 3) replace [] with () > 4) Shorten the sanitize procedure for opensmtpd-option-configuration 5) Review your class names. I also don't think it makes too much sense to add -configuration for anything but the top-level configuration record, it just requires you to type much more configuration than you probably want. Cheers From debbugs-submit-bounces@debbugs.gnu.org Thu Jul 07 13:27:18 2022 Received: (at 56046) by debbugs.gnu.org; 7 Jul 2022 17:27:18 +0000 Received: from localhost ([127.0.0.1]:57770 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o9VHa-0003Cm-9h for submit@debbugs.gnu.org; Thu, 07 Jul 2022 13:27:18 -0400 Received: from mx1.dismail.de ([78.46.223.134]:19716) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o9VHX-0003CX-Ky for 56046@debbugs.gnu.org; Thu, 07 Jul 2022 13:27:16 -0400 Received: from mx1.dismail.de (localhost [127.0.0.1]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 8a5a8c96; Thu, 7 Jul 2022 19:27:08 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; d=dismail.de; h= mime-version:date:content-type:content-transfer-encoding:from :message-id:subject:to:in-reply-to:references; s=20190914; bh=KK FthKd3VPVD4OaTYVTeHhHA4n5uu4SnNIdgq3XKyj4=; b=aFOMhEax1erZyLmV57 cxp9JQNWsd43eB4Mh4Ei2xvGILWSa3lOoe1vOG2wkq3OOH7RoS8uSkvnFq3va1hW mdsSOI2OG8dmuKUg80cEORyJCpCG4viFdvw7R+UnGRBjqTtw35CYJ2gKX4xrEXvb tYuWszs3m6n+cjxRykiB9oU9PEvUASBS6F5spU51HGMGUGGP9jF8b1ezjmzkYn4j +lsUtOD2cqMjFHFCVU1dICkaISlbw/xxKUEsufmLo49qzehdI2oWzQvTeaubMHAa 09nmYwiX/s1jtWFS3RxWmBqvjojdEXYmMLziGtf74sgAN3NMPr2Mi+lKk/mnGJQz tCgw== Received: from smtp2.dismail.de ( [10.240.26.12]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 122b0778; Thu, 7 Jul 2022 19:27:07 +0200 (CEST) Received: from smtp2.dismail.de (localhost [127.0.0.1]) by smtp2.dismail.de (OpenSMTPD) with ESMTP id 7d3c90f9; Thu, 7 Jul 2022 19:27:07 +0200 (CEST) Received: by dismail.de (OpenSMTPD) with ESMTPSA id 0992026e (TLSv1.3:TLS_AES_256_GCM_SHA384:256:NO); Thu, 7 Jul 2022 19:27:07 +0200 (CEST) MIME-Version: 1.0 Date: Thu, 07 Jul 2022 17:27:06 +0000 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable X-Mailer: RainLoop/1.16.0a From: jbranso@dismail.de Message-ID: <338cfc2ca96492218838280d8961bef7@dismail.de> Subject: Re: [PATCH] services: mail: add opensmtpd records to enhance opensmtpd-configuration. Version 2 To: "Liliana Marie Prikler" , 56046@debbugs.gnu.org In-Reply-To: References: <756a905107d7783bab238091d668fddbc1e712ab.camel@gmail.com> <20220704211759.8314-1-jbranso@dismail.de> <6a272ff438ca4e2efc8e196c9160f857@dismail.de> X-Spam-Score: -0.7 (/) X-Debbugs-Envelope-To: 56046 X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.7 (-) July 7, 2022 2:48 AM, "Liliana Marie Prikler" = wrote: > Am Mittwoch, dem 06.07.2022 um 21:51 +0000 schrieb jbranso@dismail.de: >=20 >>=20I do not believe that guile has a file-exists? thunk.=20 >>=20I could use (access? file F_OK) every time, but I think >> file-exists? is easier to use. My two cents. >=20 >=20scheme@(guile-user)> file-exists? > $1 =3D # ok. Good to know! >=20 >>=20What is a "listen-on"? >>=20 >>=20"listen-on" refers to the "listen on" in smtpd.conf: >> https://man.openbsd.org/smtpd.conf >>=20 >>=20lan_addr =3D "192.168.0.1" >> listen on $lan_addr >> listen on $lan_addr tls auth >=20 >=20Okay, but what would you call that? An address maybe? >=20 >>>=20+(define-record-type* >> configuration> >>=20 >>=20Again, could this just be ? >>=20 >>=20I would prefer to have two data types for "listen on" and >> "listen on socket". "listen on socket" only supports 3 options, >> where "listen on" supports 19. >=20 >=20I am not questioning whether it makes sense to add a configuration > record =E2=80=93 it probably does =E2=80=93 but whether you're using th= e best name for > that record. We are not Java programmers here, a little abstraction > goes a long way. >=20 >>=20From the documentation: >>=20 >>=20listen on interface [family] [options] >> Listen on the interface for incoming connections, using the same >> syntax as ifconfig(8). The interface parameter may also be an >> interface group, an IP address, or a domain name. Listening can >> optionally be restricted to a specific address family, which can be >> either inet4 or inet6. >>=20 >>=20listen on socket [options] >> Listen for incoming SMTP connections on the Unix domain socket >> /var/run/smtpd.sock. This is done by default, even if the directive >> is absent. >=20 >=20So you can either have an opensmtp-interface (with family and a bunch > of options) or an opensmtp-socket (with a bunch of options). Sounds > like a much nicer ontology, doesn't it? That does make much more sense! I'll do that. >=20 >>>=20[...] >>=20 >>=20Too much to check, too little time. Maybe return later. >>=20 To=20summarize the updated tasks that you have given me are: =20 =201) Write a proper changelog. 2) define "string-in-list?" with member? Are there other procedures that could use this? 3) DONE replace [] with () 4) Shorten the sanitize procedure for opensmtpd-option-configuration 5) Review your class names. I also don't think it makes too much sense to add -configuration for anything but the top-level configuration record, it just requires you to type much more configuration than you probably want. Also does guix have a style guide for writing services? Do we usually only put "-configuration" for the top level configuration? Is that the current style recommendations? =20 eg:=20rename opensmtpd-listen-on-configuration and opensmtpd-listen-on-socket-configuration to opensmtpd-interface and opensmtpd-socket. From debbugs-submit-bounces@debbugs.gnu.org Thu Jul 07 14:20:49 2022 Received: (at 56046) by debbugs.gnu.org; 7 Jul 2022 18:20:49 +0000 Received: from localhost ([127.0.0.1]:57860 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o9W7N-0000Vg-9v for submit@debbugs.gnu.org; Thu, 07 Jul 2022 14:20:49 -0400 Received: from mail-ed1-f65.google.com ([209.85.208.65]:42970) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o9W7L-0000VQ-D9 for 56046@debbugs.gnu.org; Thu, 07 Jul 2022 14:20:47 -0400 Received: by mail-ed1-f65.google.com with SMTP id r18so24173610edb.9 for <56046@debbugs.gnu.org>; Thu, 07 Jul 2022 11:20:47 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=message-id:subject:from:to:date:in-reply-to:references:user-agent :mime-version:content-transfer-encoding; bh=P9av8p6DIgA/4vd8NZtKc/7xvKKAHUbNrGEWeW+FL1Q=; b=ffruA5JqaiaNINHaPBL7pfi8Q4mEbJIbvznwEYxGBiU7HGh8X91T73emnGrhASZ7S/ 2wF4Xmq6tnAcEgB3XIhLCE8/Ixosywf+7ARomGq0XUc/KZq/9714B4vqW6xVUrH6xPlF hlcoGi8fmtsdJzIsHp8Ac+mlYiPz6kCP1bI721Af20tdrbS1OrruXk8gShrA30qLhjIc lKK7lN3fx80IPDecTXZXveCM+5es+AC3PWill1yF16Q6o1R20Cz7SNy5a+HYjOZ19jII DK1GGWU3k7ZyVdaScv+3L4bDosf1rQxNAWogmj9F1ARhe95mfE9hwKcMi/g3abWiwld6 2LHw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:message-id:subject:from:to:date:in-reply-to :references:user-agent:mime-version:content-transfer-encoding; bh=P9av8p6DIgA/4vd8NZtKc/7xvKKAHUbNrGEWeW+FL1Q=; b=d/3amkz98KvVysWaahrO0llvSI6DdNDkXIuZJQhpSqCEiQepAZY8cMa893Vkl7l6ek NuAn0vnqirqHiIdpwM5NHacCYwpqy+oH+tJMxZDTnA+RPSjun6q1M+RI5x5wRpSxyKJL Wk72O1LX8Rgn+ouHA6+G79O+A1YJsAllefbKJ40OdITVBDXNT50OQ1MSQNTwfCFRM2rT wk7xnSSGHIcJN6x2bnlfRtNGov+7hVv25kt3luEyc1N4vh3VSvW5Q2kz0cObVHnpsM6b YQKnNu6RoIq1RYag3vAU0eRzwJwb9/ISb9cL4b7rtCcuwGV7c7B+HiZolvyf5rac4iVQ V1lQ== X-Gm-Message-State: AJIora+2SOkWlpZQUKKJhrkL/57V1BG30UNeznlCtmL0hN7+gHVezGbR Mu/UNKnlk4sW1n3HT9bgEI4= X-Google-Smtp-Source: AGRyM1s+HEcpqf9tD511xvwBHjiIP4cy623cTEDoUgiqNUQjPTiJlQqGadqsF+RHnW8xc8Tb69rkfg== X-Received: by 2002:a05:6402:2548:b0:437:62de:668 with SMTP id l8-20020a056402254800b0043762de0668mr63740277edb.143.1657218040933; Thu, 07 Jul 2022 11:20:40 -0700 (PDT) Received: from nijino.fritz.box (85-127-52-93.dsl.dynamic.surfer.at. [85.127.52.93]) by smtp.gmail.com with ESMTPSA id ee34-20020a056402292200b0043a554818afsm10175621edb.42.2022.07.07.11.20.40 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 07 Jul 2022 11:20:40 -0700 (PDT) Message-ID: <8abb0b095fd83baa81b6dc14364ae07da4036a17.camel@gmail.com> Subject: Re: [PATCH] services: mail: add opensmtpd records to enhance opensmtpd-configuration. Version 2 From: Liliana Marie Prikler To: jbranso@dismail.de, 56046@debbugs.gnu.org Date: Thu, 07 Jul 2022 20:20:39 +0200 In-Reply-To: <338cfc2ca96492218838280d8961bef7@dismail.de> References: <756a905107d7783bab238091d668fddbc1e712ab.camel@gmail.com> <20220704211759.8314-1-jbranso@dismail.de> <6a272ff438ca4e2efc8e196c9160f857@dismail.de> <338cfc2ca96492218838280d8961bef7@dismail.de> Content-Type: text/plain; charset="UTF-8" User-Agent: Evolution 3.42.1 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Spam-Score: -0.0 (/) X-Debbugs-Envelope-To: 56046 X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) Am Donnerstag, dem 07.07.2022 um 17:27 +0000 schrieb jbranso@dismail.de: > Also does guix have a style guide for writing services?  Do we > usually only put "-configuration" for the top level configuration?  > Is that the current style recommendations? Style guide #0 is "look at what already exists". For example, the jami-service also defines jami-account via define-configuration. Hope that helps. Cheers From debbugs-submit-bounces@debbugs.gnu.org Thu Jul 07 23:06:58 2022 Received: (at 56046) by debbugs.gnu.org; 8 Jul 2022 03:06:58 +0000 Received: from localhost ([127.0.0.1]:58440 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o9eKY-000322-JU for submit@debbugs.gnu.org; Thu, 07 Jul 2022 23:06:58 -0400 Received: from mx1.dismail.de ([78.46.223.134]:37679) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1o9eKW-00031l-FL for 56046@debbugs.gnu.org; Thu, 07 Jul 2022 23:06:57 -0400 Received: from mx1.dismail.de (localhost [127.0.0.1]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 39b1e452; Fri, 8 Jul 2022 05:06:49 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; d=dismail.de; h=from:to:cc :subject:references:date:in-reply-to:message-id:mime-version :content-type:content-transfer-encoding; s=20190914; bh=x9u2KLSj ePEcG29GiEboMzFFQ3QzUD3+J5PQFBtwaJ0=; b=A27niz0MFAzcMXdXuEk3Rgh3 Dem3PXQBqyRDsc4QPEmdLcp71OIjrUMx7GPADgoWH2ai3uluEOs8GyDGC5vMabpC 7IeWB5JB6o6bDwgrpKz462roXl4Cc+kFeAcZ1vlu60NFSQ7yrH8Xgr2Vpm/mxj5+ nEsG4mzRqbcvFdQ7dbZwGkA+Bnq2xkd9dqdhjplTBSR5TdCFgEoTU3XeIFqvRGqY KjtEhDPWW3UG+BC5hseZ7JDMb+6AefxnKFhdLi+Vsu8lJJGHsEIWCbUCi5JNk16f BmwYrNk0qOKJhTgaPdHD+JHF8hCucIMkbVa/lbPLyXGaeVjH6DXCDui22X7wdA== Received: from smtp2.dismail.de ( [10.240.26.12]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 056f4131; Fri, 8 Jul 2022 05:06:48 +0200 (CEST) Received: from smtp2.dismail.de (localhost [127.0.0.1]) by smtp2.dismail.de (OpenSMTPD) with ESMTP id c1f61dc7; Fri, 8 Jul 2022 05:06:48 +0200 (CEST) Received: by dismail.de (OpenSMTPD) with ESMTPSA id 30955af7 (TLSv1.3:TLS_AES_256_GCM_SHA384:256:NO); Fri, 8 Jul 2022 05:06:47 +0200 (CEST) From: Joshua Branson To: Liliana Marie Prikler Subject: Re: bug#56046: [PATCH] services: mail: add opensmtpd records to enhance opensmtpd-configuration. References: <756a905107d7783bab238091d668fddbc1e712ab.camel@gmail.com> <20220704211759.8314-1-jbranso@dismail.de> <6a272ff438ca4e2efc8e196c9160f857@dismail.de> <338cfc2ca96492218838280d8961bef7@dismail.de> <8abb0b095fd83baa81b6dc14364ae07da4036a17.camel@gmail.com> Date: Thu, 07 Jul 2022 23:06:45 -0400 In-Reply-To: <8abb0b095fd83baa81b6dc14364ae07da4036a17.camel@gmail.com> (Liliana Marie Prikler's message of "Thu, 07 Jul 2022 20:20:39 +0200") Message-ID: <87mtdkfgl6.fsf_-_@dismail.de> User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/28.1 (gnu/linux) MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable X-Spam-Score: -0.7 (/) X-Debbugs-Envelope-To: 56046 Cc: 56046@debbugs.gnu.org X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.7 (-) Liliana Marie Prikler writes: > Am Donnerstag, dem 07.07.2022 um 17:27 +0000 schrieb > jbranso@dismail.de: >> Also does guix have a style guide for writing services?=C2=A0 Do we >> usually only put "-configuration" for the top level configuration?=C2=A0 >> Is that the current style recommendations? > Style guide #0 is "look at what already exists". For example, the > jami-service also defines jami-account via define-configuration. Hope > that helps. > I suppose that I was following the nginx style. It seems like most of their records end in "-configuration". Thanks again for reviewing this. I should have an updated patch soon. > > Cheers From debbugs-submit-bounces@debbugs.gnu.org Tue Jul 12 11:45:51 2022 Received: (at 56046) by debbugs.gnu.org; 12 Jul 2022 15:45:51 +0000 Received: from localhost ([127.0.0.1]:43557 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1oBI58-0005Th-JA for submit@debbugs.gnu.org; Tue, 12 Jul 2022 11:45:50 -0400 Received: from mx1.dismail.de ([78.46.223.134]:37764) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1oBI56-0005JZ-5q for 56046@debbugs.gnu.org; Tue, 12 Jul 2022 11:45:49 -0400 Received: from mx1.dismail.de (localhost [127.0.0.1]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 50e45c6e; Tue, 12 Jul 2022 17:45:40 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; d=dismail.de; h=from:to:cc :subject:references:date:in-reply-to:message-id:mime-version :content-type; s=20190914; bh=hsCs40vFpYWEMvHXoCXFP6Ii22t6j6yP5e aEeV9kOko=; b=mLY7TLu7AMZvEw+r3rRARhOu30cQJFNk37Th35PDADobYqCCW2 PjfZCY1sS1nYRc8XYOnkf6h3vt1ugy/gGqQoo0ROzwjyXNanZ4CAEuSACHOCEa4u xPUraprhYicByariWk0sxbI9RZudMMUOHebugq+0I6TwGz2qr91BHcgZG7u0eVm/ sJ2sWSfT+u2QYmoC8ddW0/zF5r8+hvJxjlwwsMU+cvhAK0OFjOWGKzhETNHux3zN BoByHlGqfVBDq4gpKV+iyjzxU83Nk5T1/MX9rVIvq9XKd1iB8ZSctN8cJNH0kgd9 ht6mxrbkK8mdZXL6TnwwLa4SpL89W4DXjmPQ== Received: from smtp1.dismail.de ( [10.240.26.11]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 33099331; Tue, 12 Jul 2022 17:45:40 +0200 (CEST) Received: from smtp1.dismail.de (localhost [127.0.0.1]) by smtp1.dismail.de (OpenSMTPD) with ESMTP id be2bee02; Tue, 12 Jul 2022 17:45:40 +0200 (CEST) Received: by dismail.de (OpenSMTPD) with ESMTPSA id e5236485 (TLSv1.3:TLS_AES_256_GCM_SHA384:256:NO); Tue, 12 Jul 2022 17:45:39 +0200 (CEST) From: Joshua Branson To: Liliana Marie Prikler Subject: Re: bug#56046: [PATCH] services: mail: add opensmtpd records to enhance opensmtpd-configuration. References: <756a905107d7783bab238091d668fddbc1e712ab.camel@gmail.com> <20220704211759.8314-1-jbranso@dismail.de> <6a272ff438ca4e2efc8e196c9160f857@dismail.de> <338cfc2ca96492218838280d8961bef7@dismail.de> <8abb0b095fd83baa81b6dc14364ae07da4036a17.camel@gmail.com> <87mtdkfgl6.fsf_-_@dismail.de> Date: Tue, 12 Jul 2022 11:45:37 -0400 In-Reply-To: <87mtdkfgl6.fsf_-_@dismail.de> (Joshua Branson's message of "Thu, 07 Jul 2022 23:06:45 -0400") Message-ID: <87a69etjvi.fsf_-_@dismail.de> User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/28.1 (gnu/linux) MIME-Version: 1.0 Content-Type: text/plain X-Spam-Score: -0.7 (/) X-Debbugs-Envelope-To: 56046 Cc: 56046@debbugs.gnu.org X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.7 (-) Joshua Branson writes: Just an update on progress. The main thing to report is that the best place to find the task list is here: https://notabug.org/jbranso/linode-guix-system-configuration/src/master/opensmtpd.org The best place to find a good example configuration will always be found here: https://notabug.org/jbranso/linode-guix-system-configuration/src/master/opensmtpd.org#example-configuration The main thing that I am focusing on now is how to make it easy for users of this service to use opensmtpd filters: search for "enable to do dkimsigning or bogofilter" is the file and you'll see where it is. for some reason the link is REALLY long when I try to paste it in this email. Thanks, Joshua From debbugs-submit-bounces@debbugs.gnu.org Tue Jul 12 12:38:35 2022 Received: (at 56046) by debbugs.gnu.org; 12 Jul 2022 16:38:35 +0000 Received: from localhost ([127.0.0.1]:43601 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1oBIuA-0008HD-Tn for submit@debbugs.gnu.org; Tue, 12 Jul 2022 12:38:35 -0400 Received: from mx1.dismail.de ([78.46.223.134]:22861) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1oBIu7-0008Gy-EI for 56046@debbugs.gnu.org; Tue, 12 Jul 2022 12:38:33 -0400 Received: from mx1.dismail.de (localhost [127.0.0.1]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 6671ed02; Tue, 12 Jul 2022 18:38:23 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; d=dismail.de; h=from:to:cc :subject:references:date:in-reply-to:message-id:mime-version :content-type; s=20190914; bh=o4ahJczotcAoNpZrKr39dlf9QxHaDJs6gR NdKdHJck0=; b=iHaMjtoF6FJKIq1Tvs8rsc24ejZsGPEGHi6UGJvrn5t2qxoWel fxa8uamV7ZItCPfE5sZasuVU5BOn0MnejyXVcdpfQRyfbpIIN1Rtb9yyGjdbcD3u CA9/0C2PLD+cfPO75O3jqc1RI0wCvGKXBo70Z56yRYmmxsHydSEN1/wfUxHXly4p PsFW1YpDhXoaeYwEUg0uaWWMriRgw3RUBeEXs76+iC8pvVE/+49IqquqWfg+p3m+ OSQdrfNXZciiwhi6gi5EW9djO0LJSpKygsHEQfH/gvrE6OLbcpCkUqrbuXc28Klm 2XrHWZY5TYz4xstg9YvdRmmDq1e1s4cVV84A== Received: from smtp1.dismail.de ( [10.240.26.11]) by mx1.dismail.de (OpenSMTPD) with ESMTP id e61320d3; Tue, 12 Jul 2022 18:38:23 +0200 (CEST) Received: from smtp1.dismail.de (localhost [127.0.0.1]) by smtp1.dismail.de (OpenSMTPD) with ESMTP id c8d8b956; Tue, 12 Jul 2022 18:38:23 +0200 (CEST) Received: by dismail.de (OpenSMTPD) with ESMTPSA id cd8be00c (TLSv1.3:TLS_AES_256_GCM_SHA384:256:NO); Tue, 12 Jul 2022 18:38:22 +0200 (CEST) From: Joshua Branson To: Liliana Marie Prikler Subject: Re: bug#56046: [PATCH] services: mail: add opensmtpd records to enhance opensmtpd-configuration. References: <756a905107d7783bab238091d668fddbc1e712ab.camel@gmail.com> <20220704211759.8314-1-jbranso@dismail.de> <6a272ff438ca4e2efc8e196c9160f857@dismail.de> <338cfc2ca96492218838280d8961bef7@dismail.de> <8abb0b095fd83baa81b6dc14364ae07da4036a17.camel@gmail.com> <87mtdkfgl6.fsf_-_@dismail.de> <87a69etjvi.fsf_-_@dismail.de> Date: Tue, 12 Jul 2022 12:38:20 -0400 In-Reply-To: <87a69etjvi.fsf_-_@dismail.de> (Joshua Branson's message of "Tue, 12 Jul 2022 11:45:37 -0400") Message-ID: <877d4ithfn.fsf_-_@dismail.de> User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/28.1 (gnu/linux) MIME-Version: 1.0 Content-Type: text/plain X-Spam-Score: -0.7 (/) X-Debbugs-Envelope-To: 56046 Cc: 56046@debbugs.gnu.org X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.7 (-) Joshua Branson writes: Video update! https://video.hardlimit.com/w/p/bmbYAkQ84BBfF4aAZNAPcR?playlistPosition=15&resume=true That's a link to a video update of why opensmtpd-filter-configuration-proc should be a string or list of strings and/or gexps instead of just being a mixed-text-file. It is primarily meant for nckx on irc. But anyone else is free to comment. Thanks, Joshua From debbugs-submit-bounces@debbugs.gnu.org Mon Oct 24 13:30:48 2022 Received: (at 56046) by debbugs.gnu.org; 24 Oct 2022 17:30:48 +0000 Received: from localhost ([127.0.0.1]:49414 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1on1Hk-0004xT-AJ for submit@debbugs.gnu.org; Mon, 24 Oct 2022 13:30:48 -0400 Received: from mx1.dismail.de ([78.46.223.134]:17494) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1on1Hi-0004x0-R3 for 56046@debbugs.gnu.org; Mon, 24 Oct 2022 13:30:47 -0400 Received: from mx1.dismail.de (localhost [127.0.0.1]) by mx1.dismail.de (OpenSMTPD) with ESMTP id dc4775de; Mon, 24 Oct 2022 19:30:39 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; d=dismail.de; h=from:to:cc :subject:date:message-id:mime-version:content-type :content-transfer-encoding; s=20190914; bh=tToQjv6SFJkwDTkAxH1p6 w8Mue1XJHgGpR00fbCMBOw=; b=EUDi0XILW+pHzPcqt9mZyLDW4yKwMJ2VI8IzK 9VdLgCSSbfg+RblSEAx2BffZ3npbJJMCzG7xU7Gg3uI87DFoJJ7dfxqBFhnCQmEn Czz0+ixv8fSGGMQDrj5oy3ALIPjGqAfnUiS9ktFwAfY87+kCzafs5oEtOozqN5/r lpvpLpRFGwzHXUwE/KAo/wUqb2Y75/uAaWL8/Al+gaubopXnXVNbLmm7NhWc1PFp 4SckGuO82giz9L6ajHq7DMZvYXq+2A1HniKYGiI3EJnMANAmKwYbgohXujoH32tz 6s3Uzgfqq8aiZFHBqA08X1VWqSc+8yUuswE5ceFwDVAeAKKJA== Received: from smtp1.dismail.de ( [10.240.26.11]) by mx1.dismail.de (OpenSMTPD) with ESMTP id e54dc2b4; Mon, 24 Oct 2022 19:30:39 +0200 (CEST) Received: from smtp1.dismail.de (localhost [127.0.0.1]) by smtp1.dismail.de (OpenSMTPD) with ESMTP id b5cb7152; Mon, 24 Oct 2022 19:30:39 +0200 (CEST) Received: by dismail.de (OpenSMTPD) with ESMTPSA id 6ff8272f (TLSv1.3:TLS_AES_256_GCM_SHA384:256:NO); Mon, 24 Oct 2022 19:30:36 +0200 (CEST) From: Joshua Branson To: 56046@debbugs.gnu.org Subject: [Patch master v2] services (opensmtpd): add opensmtpd records to enhance opensmtpd-configuration. Date: Mon, 24 Oct 2022 13:30:07 -0400 Message-Id: X-Mailer: git-send-email 2.38.0 MIME-Version: 1.0 Content-Type: text/plain; charset=y Content-Transfer-Encoding: 8bit X-Debbugs-Envelope-To: 56046 Cc: liliana.prikler@gmail.com, Joshua Branson X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" This is a V2 patch. I've added some tests that help test for various ways that users could accidentally misconfigure their configuration. I probably need to make those error messages, use (guix diagnostics). Currently compiling the tests, auto runs them. So "make" auto runs the tests. Also their error messages are output to the terminal, and I'm not sure how to turn that off. tl;dr this is a WIP patch, and I just wanted to submit something, because I keep finding more things that I need to fix. The task list as always is here: https://notabug.org/jbranso/linode-guix-system-configuration/src/master/opensmtpd.org Openmstpd-configuration may only be configured by a config-file that uses the smtpd.conf syntax. This patch, enables one to configure opensmtpd by using record types. * gnu/services/mail.scm: (opensmtpd-table-configuration, opensmtpd-ca-configuration, opensmtpd-pki-configuration, opensmtpd-action-local-delivery-configuration, opensmtpd-maildir-configuration, opensmtpd-mda-configuration, opensmtpd-action-relay-configuration, opensmtpd-option-configuration, opensmtpd-filter-phase-configuration, opensmtpd-filter-configuration, opensmtpd-interface, opensmtpd-socket, opensmtpd-match-configuration, opensmtpd-smtp-configuration, opensmtpd-srs-configuration, opensmtpd-queue-configuration, and opensmtpd-configuration): New records. (false?, is-value-right-type, add-comma-or-string, list-of-procedures->string, string-in-list?, my-sanitize, opensmtpd-filter-chain?, throw-error-duplicate-option, sanitize-list-of-options-for-match-configuration, sanitize-filters, list-has-duplicates-or-non-filters?, filter-phase-has-message-and-value?, filter-phase-decision-lacks-proper-message?, filter-phase-lacks-proper-value?, filter-phase-has-incorrect-junk-or-bypass?, filter-phase-junks-after-commit?, list-of-unique-filter-or-filter-phase?, throw-error, contains-duplicate?, list-of-type?, list-of-strings?, list-of-unique-opensmtpd-option-configuration?, list-of-opensmtpd-ca-configuration?, list-of-opensmtpd-pki-configuration?, list-of-opensmtpd-listen-on-configuration?, list-of-unique-opensmtpd-match-configuration?, list-of-strings->string, assoc-list? assoc-list, variable->string, table-whose-data-are-assoc-list?, table-whose-data-are-a-list-of-strings?, assoc-list->string, opensmtpd-table-configuration->string, opensmtpd-listen-on-configuration->string, opensmtpd-listen-on-socket-configuration->string, opensmtpd-action-relay-configuration->string, opensmtpd-lmtp-configuration->string, opensmtpd-mda-configuration->string, opensmtpd-maildir-configuration->string, opensmtpd-action-local-delivery-configuration->string, opensmtpd-action->string, opensmtpd-option-configuration->string, opensmtpd-match-configuration->string, opensmtpd-ca-configuration->string, opensmtpd-pki-configuration->string, generate-filter-chain-name, opensmtpd-filter-chain->string, opensmtpd-filter-phase-configuration->string, opensmtpd-filters->string, opensmtpd-configuration-listen->string, opensmtpd-configuration-srs->string, opensmtpd-smtp-configuration->string, opensmtpd-configuration-queue->string, get-opensmtpd-actions, get-opensmtpd-pki-configurations, get-opensmtpd-filters, flatten, get-opensmtpd-tables, opensmtpd-configuration-fieldname->string, list-of-records->string, opensmtpd-configuration->mixed-text-file): New procedures. * gnu/tests/mail.scm : new tests for various opensmtpd records. * doc/guix.texi (OpenSMTPD Service): Added documentation for the new records for opensmtpd. --- doc/guix.texi | 1054 ++++++++++++++++++++- gnu/services/mail.scm | 2085 ++++++++++++++++++++++++++++++++++++++++- gnu/tests/mail.scm | 355 +++++++ 3 files changed, 3475 insertions(+), 19 deletions(-) diff --git a/doc/guix.texi b/doc/guix.texi index 535c8cdfc3..c80f3e9d76 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -25409,14 +25409,59 @@ could instantiate a dovecot service like this: @subsubheading OpenSMTPD Service @deffn {Scheme Variable} opensmtpd-service-type -This is the type of the @uref{https://www.opensmtpd.org, OpenSMTPD} -service, whose value should be an @code{opensmtpd-configuration} object -as in this example: - -@lisp -(service opensmtpd-service-type - (opensmtpd-configuration - (config-file (local-file "./my-smtpd.conf")))) +OpenSMTPD is an easy-to-use mail transfer agent (MTA). Its configuration file is +throughly documented in @code{man 5 smtpd.conf}. OpenSMTPD @strong{listens} for incoming +mail and @strong{matches} the mail to @strong{actions}. The following records represent those +stages: + +@multitable {aaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @strong{listens} +@tab @code{} +@item +@tab @code{} +@item +@tab +@item @strong{matches} +@tab @code{} +@item +@tab +@item @strong{actions} +@tab @code{} +@item +@tab @code{} +@end multitable + +Additionally, each @code{} and +@code{} may use a list of +@code{}, and/or +@code{} records to filter email/spam. Also +numerous records' fieldnames use @code{} to hold lists +or key value pairs of data. + +A simple example configuration is below: + +@lisp +(let ((smtp.gnu.org (opensmtpd-pki + (domain "smtp.gnu.org") + (cert "file.cert") + (key "file.key")))) + (service opensmtpd-service-type + (opensmtpd-configuration + (listen-ons (list + (opensmtpd-interface + (pki smtp.gnu.org)) + (opensmtpd-interface + (pki smtp.gnu.org) + (secure-connection "smtps")))) + (matches (list + (opensmtpd-match + (action + (opensmtpd-local-delivery + (name "local-delivery")))) + (opensmtpd-match + (action + (opensmtpd-relay + (name "relay"))))))))) @end lisp @end deffn @@ -25433,14 +25478,1007 @@ it listens on the loopback network interface, and allows for mail from users and daemons on the local machine, as well as permitting email to remote servers. Run @command{man smtpd.conf} for more information. +<<<<<<< HEAD +@item @code{bounce} (default: @code{(list "4h")}) + +@code{bounce} is a list of strings, which send warning messages to the envelope +sender when temporary delivery failures cause a message to remain in the +queue for longer than string delay. Each string delay parameter consists +of a string beginning with a positive decimal integer and a unit 's', 'm', 'h', +or 'd'. At most four delay parameters can be specified. + +@item @code{listen-ons} (default: @code{(list (opensmtpd-interface))}) + +@code{listen-ons} is a list of @code{} records. +This list details what interfaces and ports OpenSMTPD listens on as well as +other information. + +@item @code{listen-on-socket} (default: @code{(opensmtpd-socket)}) + +Listens for incoming connections on the Unix domain socket. + +@item @code{includes} (default: @code{#f}) + +@code{includes} is a list of string filenames. Each filename's contents is +additional configuration that is inserted into the top of the configuration +file. + +@item @code{matches} default: + +@lisp + (list (opensmtpd-match + (action (opensmtpd-local-delivery + (name "local") + (method "mbox"))) + (for (opensmtpd-option + (option "for local")))) + (opensmtpd-match + (action (opensmtpd-relay + (name "outbound"))) + (from (opensmtpd-option + (option "from local"))) + (for (opensmtpd-option + (option "for any"))))) +@end lisp + +@code{matches} is a list of @code{} records, which +matches incoming mail and sends it to a correspending action. The match +records are evaluated sequentially, with the first match winning. If an +incoming mail does not match any match records, then it is rejected. +@c put this backin? @end itemize + +@c put this back in? @itemize +@item @code{mta-max-deferred} (default: @code{100}) + +When delivery to a given host is suspended due to temporary failures, cache +at most number envelopes for that host such that they can be delivered as +soon as another delivery succeeds to that host. The default is 100. + +@item @code{queue} (default: @code{#f}) + +@code{queue} expects an @code{} record. With it, one may +compress and encrypt queue-ed emails as well as set the default expiration +time for temporarily undeliverable messages. + +@item @code{smtp} (default: @code{#f}) + +@code{smtp} expects an @code{} record, which lets one +specifiy how large email may be along with other settings. + +@item @code{srs} (default: @code{#f}) + +@code{srs} expects an @code{} record, which lets one set +up SRS, the Sender Rewritting Scheme. +======= @item @code{setgid-commands?} (default: @code{#t}) Make the following commands setgid to @code{smtpq} so they can be executed: @command{smtpctl}, @command{sendmail}, @command{send-mail}, @command{makemap}, @command{mailq}, and @command{newaliases}. @xref{Setuid Programs}, for more information on setgid programs. +>>>>>>> origin/master @end table @end deftp +@itemize +@item +Data Type: opensmtpd-interface + +Data type representing the configuration of an +@code{}. Listen on the fieldname @code{interface} for +incoming connections, using the same syntax as for ifconfig(8). The interface +parameter may also be an string interface group, an string IP address, or a +string domain name. Listening can optionally be restricted to a specific +address fieldname @code{family}, which can be either ``inet4'' or ``inet6''. + +@itemize +@item @code{interface} (default: ``lo'') + +The string interface to listen for incoming connections. These interface can +usually be found by the command @code{ip link}. + +@item @code{family} (default: @code{#f}) + +The string IP family to use. Valid strings are ``inet4'' or ``inet6''. + +@item @code{auth} (default: @code{#f}) + +Support SMTPAUTH: clients may only start SMTP transactions after successful +authentication. If @code{auth} is @code{#t}, then users are authenticated against +their own normal login credentials. Alternatively @code{auth} may be an +@code{} whose users are authenticated against +their passwords. + +@item @code{auth-optional} (default: @code{#f}) + +Support SMTPAUTH optionally: clients need not authenticate, but may do so. +This allows the @code{} to both accept +incoming mail from untrusted senders and permit outgoing mail from +authenticated users (using @code{} fieldname +@code{auth}). It can be used in situations where it is not possible to listen on +a separate port (usually the submission port, 587) for users to +authenticate. + +@item @code{filters} (default: @code{#f}) + +A list of one or many @code{} or +@code{} records. The filters are applied +sequentially. These records listen and filter on connections handled by this +listener. + +@item @code{hostname} (default: @code{#f}) + +Use string ``hostname'' in the greeting banner instead of the default server +name. + +@item @code{hostnames} (default: @code{#f}) + +Override the server name for specific addresses. Use a +@code{} containing a mapping of string IP +addresses to hostnames. If the address on which the connection arrives +appears in the mapping, the associated hostname is used. + +@item @code{mask-src} (default: @code{#f}) + +If @code{#t}, then omit the from part when prepending “Received” headers. + +@item @code{disable-dsn} (default: @code{#f}) + +When @code{#t}, then disable the DSN (Delivery Status Notification) extension. + +@item @code{pki} (default: @code{#f}) + +For secure connections, use an @code{} +to prove a mail server's identity. + +@item @code{port} (default: @code{#f}) + +Listen on the integer port instead of the default port of 25. + +@item @code{proxy-v2} (default: @code{#f}) + +If @code{#t}, then support the PROXYv2 protocol, rewriting appropriately source +address received from proxy. + +@item @code{received-auth} (default: @code{#f}) + +If @code{#t}, then in “Received” headers, report whether the session was +authenticated and by which local user. + +@item @code{senders} (default: @code{#f}) + +Look up the authenticated user in the supplied +@code{} to find the email addresses that user is +allowed to submit mail as. + +@item @code{secure-connection} (default: @code{#f}) + +This is a string of one of these options: + +@multitable {aaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item ``smtps'' +@tab Support SMTPS, by default on port 465. +@item ``tls'' +@tab Support STARTTLS, by default on port 25. +@item ``tls-require-verify'' +@tab Like tls, but force clients to establish +@item +@tab a secure connection before being allowed to +@item +@tab start an SMTP transaction. With the verify +@item +@tab option, clients must also provide a valid +@item +@tab certificate to establish an SMTP session. +@end multitable + +@item @code{tag} (default: @code{#f}) + +Clients connecting to the listener are tagged with the given string tag. +@end itemize + +@item Data Type: opensmtpd-socket + +Data type representing the configuration of an +@code{}. Listen for incoming SMTP +connections on the Unix domain socket @samp{/var/run/smtpd.sock}. This is done by +default, even if the directive is absent. + +@itemize +@item @code{filters} (default: @code{#f}) + +A list of one or many @code{} or +@code{} records. These filter incoming +connections handled by this listener. + +@item @code{mask-src} (default: @code{#f}) + +If @code{#t}, then omit the from part when prepending “Received” headers. + +@item @code{tag} (default: @code{#f}) + +Clients connecting to the listener are tagged with the given string tag. +@end itemize + +@item Data Type: opensmtpd-match + +This data type represents the configuration of an +@code{} record. + +If at least one mail envelope matches the options of one match record, receive +the incoming message, put a copy into each matching envelope, and atomically +save the envelopes to the mail spool for later processing by the respective +@code{} found in fieldname @code{action}. + +@itemize +@item @code{action} (default: @code{#f}) + +If mail matches this match configuration, then do this action. Valid values +include @code{} or +@code{}. + +@item @code{options} (default: @code{#f}) @code{} +The fieldname 'option' is a list of unique +@code{} records. + +Each @code{} record's fieldname 'option' has some +mutually exclusive options: there can be only one ``for'' and only one ``from'' option. + +@multitable {aaaaaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@headitem for +@tab from +@item only use one of the following: +@tab only use one of the following: +@item ``for any'' +@tab ``from any'' +@item ``for local'' +@tab ``from auth'' +@item ``for domain'' +@tab ``from local'' +@item ``for rcpt-to'' +@tab ``from mail-from'' +@item +@tab ``from socket'' +@item +@tab ``from src'' +@end multitable + +The following matching options are supported and can all be negated (via not +#t). The options that support a table (anything surrounded with '<' and '>' +eg:
), also support specifying regex via (regex #t). + +@itemize +@item @samp{for any} + +Specify that session may address any destination. + +@item @samp{for local} + +Specify that session may address any local domain. This is the default, +and may be omitted. + +@item @samp{for domain _domain_ | } + +Specify that session may address the string or list table domain. + +@item @samp{for rcpt-to _recipient_ | } + +Specify that session may address the string or list table recipient. + +@item @samp{from any} + +Specify that session may originate from any source. + +@item @samp{from auth} + +Specify that session may originate from any authenticated user, no matter +the source IP address. + +@item @samp{from auth _user_ | } + +Specify that session may originate from authenticated user or user list +user, no matter the source IP address. + +@item @samp{from local} + +Specify that session may only originate from a local IP address, or from +the local enqueuer. This is the default, and may be omitted. + +@item @samp{from mail-from _sender_ | } + +Specify that session may originate from sender or table sender, no +matter the source IP address. + +@item @samp{from rdns} + +Specify that session may only originate from an IP address that resolves +to a reverse DNS@. + +@item @samp{from rdns _hostname_ | } + +Specify that session may only originate from an IP address that resolves +to a reverse DNS matching string or list string hostname. + +@item @samp{from socket} + +Specify that session may only originate from the local enqueuer. + +@item @samp{from src _address_ |
} + +Specify that session may only originate from string or list table address +which can be a specific address or a subnet expressed in CIDR-notation. + +@item @samp{auth} + +Matches transactions which have been authenticated. + +@item @samp{auth _username_ | } + +Matches transactions which have been authenticated for user or user list +username. + +@item @samp{helo _helo-name_ | } + +Specify that session's HELO / EHLO should match the string or list table +helo-name. + +@item @samp{mail-from _sender_ | } + +Specify that transactions's MAIL FROM should match the string or list +table sender. + +@item @samp{rcpt-to _recipient_ | } + +Specify that transaction's RCPT TO should match the string or list table +recipient. + +@item @samp{tag tag} +Matches transactions tagged with the given tag. + +@item @samp{tls} +Specify that transaction should take place in a TLS channel. +@end itemize + +Here is a simple example: +@lisp + (opensmtpd-option + (not #t) + (regex #f) + (option "for domain") + (data (opensmtpd-table + (name "domain-table") + (data (list "gnu.org" "dismail.de"))))) +@end lisp + +The mail must NOT come from the domains @samp{gnu.org} or @samp{dismail.de}. + +@item Data Type: opensmtpd-option +@end itemize + +@item Data Type: opensmtpd-local-delivery + +This data type represents the configuration of an +@code{} record. + +@itemize +@item +@code{name} (default: @code{#f}) + +@code{name} is the string name of the relay action. + +@item @code{method} (default: @code{"mbox"}) + +The email delivery option. Valid options are: + +@itemize +@item @code{"mbox"} + +Deliver the message to the user's mbox with mail.local(8). + +@item @code{"expand-only"} + +Only accept the message if a delivery method was specified in an aliases +or .forward file. + +@item @code{"forward-only"} + +Only accept the message if the recipient results in a remote address after +the processing of aliases or forward file. + +@item @code{} + +Deliver the message to an LMTP server at +@code{}'s fieldname @code{destination}. The location +may be expressed as string host:port or as a UNIX socket. Optionally, +@code{}'s fieldname @code{rcpt-to} might be specified +to use the recipient email address (after expansion) instead of the local +user in the LMTP session as RCPT TO@. + +@item @code{} + +Deliver the message to the maildir in +@code{}'s fieldname @code{pathname} if specified, +or by default to @samp{~/Maildir}. + +The pathname may contain format specifiers that are expanded before use +(see the below section about Format Specifiers). + +If @code{}'s record fieldname @code{junk} is @code{#t}, +then message will be moved to the ‘Junk’ folder if it contains a positive +‘X-Spam’ header. This folder will be created under fieldname @code{pathname} if +it does not yet exist. + +@item @code{} + +Delegate the delivery to the @code{}'s fieldname +@code{command} (type string) that receives the message on its standard input. + +The @code{command} may contain format specifiers that are expanded before use +(see Format Specifiers). +@end itemize + +@item @code{alias} (default: @code{#f}) + +Use the mapping table for aliases expansion. @code{alias} is an +@code{}. + +@item @code{ttl} (default: @code{#f}) + +@code{ttl} is a string specify how long a message may remain in the queue. It's +format is @samp{n@{s|m|h|d@}}. eg: ``4m'' is four minutes. + +@item @code{user} (default: @code{#f} ) + +@code{user} is the string username for performing the delivery, to be looked up +with getpwnam(3). + +This is used for virtual hosting where a single username is in charge of +handling delivery for all virtual users. + +This option is not usable with the mbox delivery method. + +@item @code{userbase} (default: @code{#f}) + +@code{userbase} is an @code{} record for mapping user +lookups instead of the getpwnam(3) function. + +The fieldnames @code{user} and @code{userbase} are mutually exclusive. + +@item @code{virtual} (default: @code{#f}) + +@code{virtual} is an @code{} record is used for virtual +expansion. +@end itemize + +@item Data Type: opensmtpd-relay + +This data type represents the configuration of an +@code{} record. + +@itemize +@item @code{name} (default: @code{#f}) + +@code{name} is the string name of the relay action. + +@item @code{backup} (default: @code{#f}) + +When @code{#t}, operate as a backup mail exchanger delivering messages to any +mail exchanger with higher priority. + +@item @code{backup-mx} (default: @code{#f}) + +Operate as a backup mail exchanger delivering messages to any mail exchanger +with higher priority than mail exchanger identified as string name. + +@item @code{helo} (default: @code{#f}) + +Advertise string heloname as the hostname to other mail exchangers during +the HELO phase. + +@item @code{helo-src} (default: @code{#f} ) + + Use the mapping @code{} to look up a hostname +matching the source address, to advertise during the HELO phase. + +@item @code{domain} (default: @code{#f}) + +Do not perform MX lookups but look up destination domain in an +@code{} and use matching relay url as relay host. + +@item @code{host} (default: @code{#f}) + +Do not perform MX lookups but relay messages to the relay host described by +the string relay-url. The format for relay-url is +@samp{[proto://[label@@]]host[:port]}. The following protocols are available: + +@multitable {aaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item smtp +@tab Normal SMTP session with opportunistic STARTTLS (the default). +@item smtp+tls +@tab Normal SMTP session with mandatory STARTTLS@. +@item smtp+notls +@tab Plain text SMTP session without TLS@. +@item lmtp +@tab LMTP session. port is required. +@item smtps +@tab SMTP session with forced TLS on connection, default port is +@item +@tab 465. +@end multitable + +Unless noted, port defaults to 25. + +The label corresponds to an entry in a credentials table, as documented in +@samp{table(5)}. It is used with the @samp{"smtp+tls"} and @samp{"smtps"} protocols for +authentication. Server certificates for those protocols are verified by +default. + +@item @code{pki} (default: @code{#f}) + +For secure connections, use the certificate associated with +@code{} (declared in a pki directive) to prove the +client's identity to the remote mail server. + +@item @code{srs} (default: @code{#f}) + +If @code{#t}, then when relaying a mail resulting from a forward, use the Sender +Rewriting Scheme to rewrite sender address. + +@item @code{tls} (default: @code{#f}) boolean or string ``no-verify'' + +When @code{#t}, Require TLS to be used when relaying, using mandatory STARTTLS by +default. When used with a smarthost, the protocol must not be +@samp{"smtp+notls://"}. When string @code{"no-verify"}, then do not require a valid +certificate. + +@item @code{auth} (default: @code{#f}) @code{} + +Use the alist @code{} for connecting to relay-url +using credentials. This option is usable only with fieldname @code{host} option. + +@item @code{mail-from} (default: @code{#f}) string + +Use the string mailaddress as MAIL FROM address within the SMTP transaction. + +@item @code{src} (default: @code{#f}) string | @code{} + +Use the string or @code{} sourceaddr for the +source IP address, which is useful on machines with multiple interfaces. If +the list contains more than one address, all of them are used in such a way +that traffic is routed as efficiently as possible. +@end itemize + +@item Data Type: opensmtpd-filter + +This data type represents the configuration of an +@code{}. This is the filter record one should use +if they want to use an external package to filter email eg: rspamd or +spamassassin. + +@itemize +@item @code{name} (default: @code{#f}) + +The string name of the filter. + +@item @code{proc} (default: @code{#f}) + +The string command or process name. If @code{proc-exec} is @code{#t}, @code{proc} is +treated as a command to execute. Otherwise, it is a process name. + +@item @code{proc-exec} (default: @code{#f}) +@end itemize + +@item Data Type: opensmtpd-filter-phase + +This data type represents the configuration of an +@code{}. + +In a regular workflow, smtpd(8) may accept or reject a message based only on +the content of envelopes. Its decisions are about the handling of the message, +not about the handling of an active session. + +Filtering extends the decision making process by allowing smtpd(8) to stop at +each phase of an SMTP session, check that options are met, then decide if a +session is allowed to move forward. + +With filtering via an @code{} record, a +session may be interrupted at any phase before an envelope is complete. A +message may also be rejected after being submitted, regardless of whether the +envelope was accepted or not. + +@itemize +@item @code{name} (default: @code{#f}) + +The string name of the filter phase. + +@item @code{phase-name} (default: @code{#f}) + +The string name of the phase. Valid values are: + +@multitable {aaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item ``connect'' +@tab upon connection, before a banner is displayed +@item ``helo'' +@tab after HELO command is submitted +@item ``ehlo'' +@tab after EHLO command is submitted +@item ``mail-from'' +@tab after MAIL FROM command is submitted +@item ``rcpt-to'' +@tab after RCPT TO command is submitted +@item ``data'' +@tab after DATA command is submitted +@item ``commit'' +@tab after message is fully is submitted +@end multitable + +@item @code{options} (default @code{#f}) + +A list of unique @code{} records. + +At each phase, various options, specified by a list of +@code{}, may be checked. The +@code{}'s fieldname 'option' values of: ``fcrdns'', +``rdns'', and ``src'' data are available in all phases, but other data must have +been already submitted before they are available. Options with a @samp{
} +next to them require the @code{}'s fieldname +@code{data} to be an @code{}. There are the available +options: + +@multitable {aaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item fcrdns +@tab forward-confirmed reverse DNS is valid +@item rdns +@tab session has a reverse DNS +@item rdns
+@tab session has a reverse DNS in table +@item src
+@tab source address is in table +@item helo
+@tab helo name is in table +@item auth +@tab session is authenticated +@item auth
+@tab session username is in table +@item mail-from
+@tab sender address is in table +@item rcpt-to
+@tab recipient address is in table +@end multitable + +These conditions may all be negated by setting +@code{}'s fieldname @code{not} to @code{#t}. + +Any conditions that require a table may indicate that tables include regexs +setting @code{}'s fieldname @code{regex} to @code{#t}. + +@item @code{decision} + +A string decision to be taken. Some decisions require an @code{message} or +@code{value}. Valid strings are: + +@multitable {aaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item ``bypass'' +@tab the session or transaction bypasses filters +@item ``disconnect'' message +@tab the session is disconnected with message +@item ``junk'' +@tab the session or transaction is junked, i.e., an +@item +@tab ‘X-Spam: yes’ header is added to any messages +@item ``reject'' message +@tab the command is rejected with message +@item ``rewrite'' value +@tab the command parameter is rewritten with value +@end multitable + +Decisions that involve a message require that the message be RFC valid, +meaning that they should either start with a 4xx or 5xx status code. +Descisions can be taken at any phase, though junking can only happen before +a message is committed. + +@item @code{message} (default @code{#f}) + +A string message beginning with a 4xx or 5xx status code. + +@item @code{value} (default: @code{#f}) + +A number value. @code{value} and @code{message} are mutually exclusive. +@end itemize + +@item Data Type: opensmtpd-option + +This data type represents the configuration of an +@code{}, which is used by +@code{} and @code{} +to match various options for email. + +@itemize +@item @code{conditition} (default @code{#f}) + +A string option to be taken. Some options require a string or an +@code{} via the fieldname data. When the option +record is used inside of an @code{}, then +valid strings are: + +At each phase, various options may be matched. The fcrdns, rdns, and src +data are available in all phases, but other data must have been already +submitted before they are available. + +@multitable {aaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item ``fcrdns'' +@tab forward-confirmed reverse DNS is valid +@item ``rdns'' +@tab session has a reverse DNS +@item ``rdns''
+@tab session has a reverse DNS in table +@item ``src''
+@tab source address is in table +@item ``helo''
+@tab helo name is in table +@item ``auth'' +@tab session is authenticated +@item ``auth''
+@tab session username is in table +@item ``mail-from''
+@tab sender address is in table +@item ``rcpt-to''
+@tab recipient address is in table +@end multitable + +When @code{} is used inside of an +@code{}, then valid strigs for fieldname @code{option} +are: ``for'', ``for any'', ``for local'', ``for domain'', ``for rcpt-to'', ``from any'' +``from auth'', ``from local'', ``from mail-from'', ``from rdns'', ``from socket'', +``from src'', ``auth'', ``helo'', ``mail-from'', ``rcpt-to'', ``tag'', or ``tls''. + +@item @code{data} (default @code{#f}) @code{} + +Some options require a table to be present. One would specify that table +here. +@item @code{regex} (default: @code{#f}) boolean + +Any options using a table may indicate that tables hold regex by +prefixing the table name with the keyword regex. + +@item @code{not} (default: @code{#f}) boolean + +When @code{#t}, this option record is negated. +@end itemize + +@item Data Type: opensmtpd-table + +This data type represents the configuration of an +@code{}. + +@itemize +@item @code{name} (default @code{#f}) + +@code{name} is the name of the @code{} record. + +@item @code{data} (default: @code{#f}) + +@code{data} expects a list of strings or an alist, which is a list of +cons cells. eg: @code{(data (list ("james" . "password")))} OR +@code{(data (list ("gnu.org" "fsf.org")))}. +@end itemize + +@item Data Type: opensmtpd-pki + +This data type represents the configuration of an +@code{}. + +@itemize +@item @code{domain} (default @code{#f}) + +@code{domain} is the string name of the @code{} record. + +@item @code{cert} (default: @code{#f}) + +@code{cert} (default: @code{#f}) + +@code{cert} is the string certificate filename to use for this pki. + +@item @code{key} (default: @code{#f}) + +@code{key} is the string certificate falename to use for this pki. + +@item @code{dhe} (default: @code{"none"}) + +Specify the DHE string parameter to use for DHE cipher suites with host +pkiname. Valid parameter values are ``none'', ``legacy'', or ``auto''. For ``legacy'', a +fixed key length of 1024 bits is used, whereas for ``auto'', the key length is +determined automatically. The default is ``none'', which disables DHE cipher +suites. +@end itemize + +@item Data Type: opensmtpd-maildir + +@itemize +@item @code{pathname} (default: @code{"~/Maildir"}) + +Deliver the message to the maildir if pathname if specified, or by default +to @samp{~/Maildir}. + +The pathname may contain format specifiers that are expanded before use +(see FORMAT SPECIFIERS). + +@item @code{junk} (default: @code{#f}) + +If the junk argument is @code{#t}, then the message will be moved to the @samp{‘Junk’} +folder if it contains a positive @samp{‘X-Spam’} header. This folder will be +created under pathname if it does not yet exist. +@end itemize + +@item Data Type: opensmtpd-mda + +@itemize +@item @code{name} + +The string name for this MDA command. + +@item @code{command} + +Delegate the delivery to a command that receives the message on its standard +input. + +The command may contain format specifiers that are expanded before use (see +FORMAT SPECIFIERS). +@end itemize + +@item Data Type: opensmtpd-queue + +@itemize +@item @code{compression} (default @code{#f}) + +Store queue files in a compressed format. This may be useful to save disk +space. + +@item @code{encryption} (default @code{#f}) + +Encrypt queue files with EVP@math{_aes}@math{_256}@math{_gcm}(3). If no key is specified, it is +read with getpass(3). If the string stdin or a single dash (‘-’) is given +instead of a key, the key is read from the standard input. + +@item @code{ttl-delay} (default @code{#f}) + +Set the default expiration time for temporarily undeliverable messages, +given as a positive decimal integer followed by a unit s, m, h, or d. The +default is four days (``4d''). +@end itemize + +@item Data Type: opensmtpd-smtp + +Data type representing an @code{} record. + +@itemize +@item @code{ciphers} (default: @code{#f}) + +Set the control string for SSL@math{_CTX}@math{_set}@math{_cipher}@math{_list}(3). The default is + ``HIGH:!aNULL:!MD5''. + +@item @code{limit-max-mails} (default: @code{100}) + +Limit the number of messages to count for each sessio + +@item @code{limit-max-rcpt} (default: @code{1000}) + +Limit the number of recipients to count for each transaction. + +@item @code{max-message-size} (default: @code{35M}) + +Reject messages larger than size, given as a positive number of bytes or as +a string to be parsed with scan@math{_scaled}(3). + +@item @code{sub-addr-delim character} (default: @code{+}) + +When resolving the local part of a local email address, ignore the ASCII +character and all characters following it. This is helpful for email +filters. @samp{"admin+bills@@gnu.org"} is the same email address as +@samp{"admin@@gnu.org"}. BUT an email filter can filter emails addressed to first +email address into a 'Bills' email folder. +@end itemize + +@item Data Type: opensmtpd-srs + +@itemize +@item @code{key} (default: @code{#f}) + +Set the secret key to use for SRS, the Sender Rewriting Scheme. + +@item @code{backup-key} (default: @code{#f}) + +Set a backup secret key to use as a fallback for SRS@. This can be used to +implement SRS key rotation. + +@item @code{ttl-delay} (default: @code{"4d"}) + +Set the time-to-live delay for SRS envelopes. After this delay, a bounce +reply to the SRS address will be discarded to limit risks of forged +addresses. +@end itemize + +@item Format Specifiers + +Some configuration records support expansion of their parameters at +runtime. Such records (for example +@code{}, @code{}) may use +format specifiers which are expanded before delivery or relaying. The +following formats are currently supported: + +@multitable {aaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @samp{%@{sender@}} +@tab sender email address, may be empty string +@item @samp{%@{sender.user@}} +@tab user part of the sender email address, may be empty +@item @samp{%@{sender.domain@}} +@tab domain part of the sender email address, may be empty +@item @samp{%@{rcpt@}} +@tab recipient email address +@item @samp{%@{rcpt.user@}} +@tab user part of the recipient email address +@item @samp{%@{rcpt.domain@}} +@tab domain part of the recipient email address +@item @samp{%@{dest@}} +@tab recipient email address after expansion +@item @samp{%@{dest.user@}} +@tab user part after expansion +@item @samp{%@{dest.domain@}} +@tab domain part after expansion +@item @samp{%@{user.username@}} +@tab local user +@item @samp{%@{user.directory@}} +@tab home directory of the local user +@item @samp{%@{mbox.from@}} +@tab name used in mbox From separator lines +@item @samp{%@{mda@}} +@tab mda command, only available for mda wrappers +@end multitable + +Expansion formats also support partial expansion using the optional bracket notations +with substring offset. For example, with recipient domain @samp{“example.org”}: + +@multitable {aaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaa} +@item @samp{%@{rcpt.domain[0]@}} +@tab expands to “e” +@item @samp{%@{rcpt.domain[1]@}} +@tab expands to “x” +@item @samp{%@{rcpt.domain[8:]@}} +@tab expands to “org” +@item @samp{%@{rcpt.domain[-3:]@}} +@tab expands to “org” +@item @samp{%@{rcpt.domain[0:6]@}} +@tab expands to “example” +@item @samp{%@{rcpt.domain[0:-4]@}} +@tab expands to “example” +@end multitable + +In addition, modifiers may be applied to the token. For example, with recipient +@samp{“User+Tag@@Example.org”}: + +@multitable {aaaaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @samp{%@{rcpt:lowercase@}} +@tab expands to “user+tag@@example.org” +@item @samp{%@{rcpt:uppercase@}} +@tab expands to “USER+TAG@@EXAMPLE.ORG” +@item @samp{%@{rcpt:strip@}} +@tab expands to “User@@Example.org” +@item @samp{%@{rcpt:lowercasestrip@}} +@tab expands to “user@@example.org” +@end multitable + +For security concerns, expanded values are sanitized and potentially dangerous +characters are replaced with ‘:’. In situations where they are desirable, the +“raw” modifier may be applied. For example, with recipient +@samp{“user+t?g@@example.org”}: + +@multitable {aaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @samp{%@{rcpt@}} +@tab expands to “user+t:g@@example.org” +@item @samp{%@{rcpt:raw@}} +@tab expands to “user+t?g@@example.org” +@end multitable +@end itemize + @subsubheading Exim Service @cindex mail transfer agent (MTA) diff --git a/gnu/services/mail.scm b/gnu/services/mail.scm index 43f144a42d..d86e083d19 100644 --- a/gnu/services/mail.scm +++ b/gnu/services/mail.scm @@ -58,10 +58,146 @@ (define-module (gnu services mail) mailbox-configuration namespace-configuration + opensmtpd-table + opensmtpd-table? + opensmtpd-table-name + opensmtpd-table-file-db + opensmtpd-table-data + + opensmtpd-ca + opensmtpd-ca? + opensmtpd-ca-name + opensmtpd-ca-file + + opensmtpd-pki + opensmtpd-pki? + opensmtpd-pki-domain + opensmtpd-pki-cert + opensmtpd-pki-key + opensmtpd-pki-dhe + + opensmtpd-local-delivery + opensmtpd-local-delivery? + opensmtpd-local-delivery-method + opensmtpd-local-delivery-alias + opensmtpd-local-delivery-ttl + opensmtpd-local-delivery-user + opensmtpd-local-delivery-userbase + opensmtpd-local-delivery-virtual + opensmtpd-local-delivery-wrapper + + opensmtpd-maildir + opensmtpd-maildir? + opensmtpd-maildir-pathname + opensmtpd-maildir-junk + + opensmtpd-mda + opensmtpd-mda-name + opensmtpd-mda-command + + opensmtpd-lmtp + opensmtpd-lmtp-destination + opensmtpd-lmtp-rcpt + + opensmtpd-relay + opensmtpd-relay? + opensmtpd-relay-backup + opensmtpd-relay-backup-mx + opensmtpd-relay-helo + opensmtpd-relay-domain + opensmtpd-relay-host + opensmtpd-relay-pki + opensmtpd-relay-srs + opensmtpd-relay-tls + opensmtpd-relay-auth + opensmtpd-relay-mail-from + opensmtpd-relay-src + + opensmtpd-option + opensmtpd-option? + opensmtpd-option-option + opensmtpd-option-not + opensmtpd-option-regex + opensmtpd-option-data + + opensmtpd-filter-phase + opensmtpd-filter-phase? + opensmtpd-filter-phase-name + opensmtpd-filter-phase-phase-name + opensmtpd-filter-phase-options + opensmtpd-filter-phase-decision + opensmtpd-filter-phase-message + opensmtpd-filter-phase-value + + opensmtpd-filter + opensmtpd-filter? + opensmtpd-filter-name + opensmtpd-filter-proc + + opensmtpd-interface + opensmtpd-interface? + opensmtpd-interface-interface + opensmtpd-interface-family + opensmtpd-interface-auth + opensmtpd-interface-auth-optional + opensmtpd-interface-filters + opensmtpd-interface-hostname + opensmtpd-interface-hostnames + opensmtpd-interface-mask-src + opensmtpd-interface-disable-dsn + opensmtpd-interface-pki + opensmtpd-interface-port + opensmtpd-interface-proxy-v2 + opensmtpd-interface-received-auth + opensmtpd-interface-senders + opensmtpd-interface-secure-connection + opensmtpd-interface-tag + + opensmtpd-socket + opensmtpd-socket? + opensmtpd-socket-filters + opensmtpd-socket-mask-src + opensmtpd-socket-tag + + opensmtpd-match + opensmtpd-match? + opensmtpd-match-action + opensmtpd-match-options + + opensmtpd-smtp + opensmtpd-smtp? + opensmtpd-smtp-ciphers + opensmtpd-smtp-limit-max-mails + opensmtpd-smtp-limit-max-rcpt + opensmtpd-smtp-max-message-size + opensmtpd-smtp-sub-addr-delim character + + opensmtpd-srs + opensmtpd-srs? + opensmtpd-srs-key + opensmtpd-srs-backup-key + opensmtpd-srs-ttl-delay + + opensmtpd-queue + opensmtpd-queue? + opensmtpd-queue-compression + opensmtpd-queue-encryption + opensmtpd-queue-ttl-delay + opensmtpd-configuration opensmtpd-configuration? - opensmtpd-service-type - %default-opensmtpd-config-file + opensmtpd-package + opensmtpd-config-file + opensmtpd-configuration-bounce + opensmtpd-configuration-listen-ons + opensmtpd-configuration-listen-on-socket + opensmtpd-configuration-includes + opensmtpd-configuration-matches + opensmtpd-configuration-mda-wrappers + opensmtpd-configuration-mta-max-deferred + opensmtpd-configuration-srs + opensmtpd-configuration-smtp + opensmtpd-configuration-queue mail-aliases-service-type @@ -1641,22 +1777,1942 @@ (define (generate-dovecot-documentation) (listeners unix-listener-configuration fifo-listener-configuration inet-listener-configuration)) (protocol-configuration ,protocol-configuration-fields)) - 'dovecot-configuration)) + 'dovecot-configuration)) ;;; ;;; OpenSMTPD. ;;; +;; file-exists? is in the guile standard library. BUT I errors if its arg +;; is a list. eg: (file-exists? (list "hello" "hello")) +;; TODO I need to find a way to remove this definition and rewrite my code. +(define (file-exists? file) + (if (string? file) + (access? file F_OK) + #f)) + +;; some fieldnames have a default value of #f, which is ok. They cannot have a value of #t. +;; for example opensmtpd-table-data can be #f, BUT NOT true. +;; my/sanitize procedure tests values to see if they are of the right kind. +;; procedure false? is needed to allow fields like 'values' to be blank, (empty), or #f BUT also +;; have a value like a list of strings. +(define (false? var) + (eq? #f var)) + +;; this procedure takes in a var and a list of procedures. It loops through list of procedures passing in var to each. +;; if one procedure returns #t, the function returns true. Otherwise #f. +;; TODO for fun rewrite this using map +;; If I rewrote it in map, then it may help with sanitizing. +;; eg: I could then potentially easily sanitize vars with lambda procedures. +(define (is-value-right-type? var list-of-procedures record fieldname) + (if (null? list-of-procedures) + #f + (if ((car list-of-procedures) var) + #t + (is-value-right-type? var (cdr list-of-procedures) record fieldname)))) + +;; converts strings like this: +;; "apple, ham, cherry" -> "apple, ham, or cherry" +;; "pineapple" -> "pinneapple". +;; "cheese, grapefruit, or jam" -> "cheese, grapefruit, or jam" +(define (add-comma-or string) + (define last-comma-location (string-rindex string #\,)) + (if last-comma-location + (if (string-contains string ", or" last-comma-location) + string + (string-replace string ", or" last-comma-location + (+ 1 last-comma-location))) + string)) + +(define (list-of-procedures->string procedures) + (define string + (let loop ((procedures procedures)) + (if (null? procedures) + "" + (begin + (string-append + (cond ((eq? false? (car procedures)) + "#f , ") + ((eq? boolean? (car procedures)) + "boolean, ") + ((eq? string? (car procedures)) + "string, ") + ((eq? integer? (car procedures)) + "integer, ") + ((eq? list-of-strings? (car procedures)) + "list of strings, ") + ((eq? assoc-list? (car procedures)) + "an association list, ") + ((eq? opensmtpd-pki? (car procedures)) + "an record, ") + ((eq? opensmtpd-table? (car procedures)) + "an record, ") + ((eq? list-of-unique-opensmtpd-match? (car procedures)) + "a list of unique records, ") + ((eq? list-of-strings-or-gexps? (car procedures)) + "a list of strings or gexps, ") + ((eq? table-whose-data-are-assoc-list? (car procedures)) + (string-append + "an record whose fieldname 'data' are an assoc-list \n" + "(eg: (opensmtpd-table (name \"hostnames\") (data '((\"124.394.23.1\" . \"gnu.org\"))))), ")) + ((eq? file-exists? (car procedures)) + "file, ") + (else "has an incorrect value, ")) + (loop (cdr procedures))))))) + (add-comma-or (string-append (string-drop-right string 2) ".\n"))) + +(define (string-in-list? string list) + (member string list)) + +(define (list-of-strings-or-gexps? list) + (and (list? list) + (cond ((null? list) + #t) + ((or (string? (car list)) + (gexp? (car list)) + (local-file? (car list)) + (file-append? (car list)) + (plain-file? (car list)) + (computed-file? (car list)) + (program-file? (car list))) + (list-of-strings-or-gexps? (cdr list))) + (else #f)))) + +(define (my/sanitize var record fieldname list-of-procedures) + (if (is-value-right-type? var list-of-procedures record fieldname) + var + (begin + (display (string-append "<" record "> fieldname: '" fieldname "' is of type " + (list-of-procedures->string list-of-procedures) "\n")) + (throw 'bad! var)))) + +;; Some example opensmtpd-tables: +;; +;; (opensmtpd-table (name "root accounts") (data '(("joshua" . "root@dismail.de") ("joshua" . "postmaster@dismail.de")))) +;; (opensmtpd-table (name "root accounts") (data (list "mysite.me" "your-site.com"))) +;; TODO should support have a fieldname 'file'? +;; Or should I change name to name-or-file ? +(define-record-type* + opensmtpd-table make-opensmtpd-table + opensmtpd-table? + this-record + (name opensmtpd-table-name ;; string + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-table" "name" (list string?))))) + (file-db opensmtpd-table-file-db + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-table" "file-db" + (list boolean?))))) + ;; FIXME support an aliasing table as described here: + ;; https://man.openbsd.org/table.5 + ;; One may have to use the record file for this. I don't think tables support a table like this: + ;; table "name" { joshua = joshua@gnucode.me,joshua@gnu-hurd.com,joshua@propernaming.org, root = root@gnucode.me } + ;; If values is an absolute filename, then it will use said filename to house the table info. + ;; filename must be an absolute filename. + (data opensmtpd-table-data + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-table" "values" + (list list-of-strings? assoc-list? file-exists?))))) + ;; is a list of values or key values + ;; eg: (list "mysite.me" "your-site.com") + ;; eg: (list ("joshua" . "joshua@gnu.org") ("james" . "james@gnu.org")) + ;; I am currently making these values be as assocation list of strings only. + ;; FIXME should I allow a var like this? + ;; (list (cons "gnucode.me" 234.949.392.23)) + ;; can be of type: (quote list-of-strings) or (quote assoc-list) + ;; (opensmtpd-table-type record) returns the values' type. The user SHOULD NEVER set the type. + ;; TODO jpoiret: on irc reccomends that I just use an outside function to determine fieldname 'values', type. + ;; it would be "simpler" and possibly easier for the next person working on this code to understand what is happening. + (type opensmtpd-table-type + (default #f) + (thunked) + (sanitize (lambda (var) + (cond ((opensmtpd-table-data this-record) + (if (list-of-strings? (opensmtpd-table-data this-record)) + (quote list-of-strings) + (quote assoc-list))) + ((file-exists? (opensmtpd-table-data this-record)) + (if (opensmtpd-table-file-db this-record) + (quote db) + (quote file))) + (else + (display "opensmtpd-table-type is broke\n") + (throw 'bad! var))))))) + +(define-record-type* + opensmtpd-ca make-opensmtpd-ca + opensmtpd-ca? + (name opensmtpd-ca-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-ca" "name" (list string?))))) + (file opensmtpd-ca-file + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-ca" "file" (list file-exists?)))))) + +(define-record-type* + opensmtpd-pki make-opensmtpd-pki + opensmtpd-pki? + (domain opensmtpd-pki-domain + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-pki" "domain" (list string?))))) + ;; TODO/FIXME this should probably be a list of files. The opensmtpd documentation says + ;; that you could have a list of files: + ;; + ;; pki pkiname cert certfile + ;; Associate certificate file certfile with host pkiname, and use that file to prove + ;; the identity of the mail server to clients. pkiname is the server's name, de‐ + ;; rived from the default hostname or set using either + ;; /gnu/store/2d13sdz76ldq8zgwv4wif0zx7hkr3mh2-opensmtpd-6.8.0p2/etc/mailname or us‐ + ;; ing the hostname directive. If a fallback certificate or SNI is wanted, the ‘*’ + ;; wildcard may be used as pkiname. + + ;; A certificate chain may be created by appending one or many certificates, includ‐ + ;; ing a Certificate Authority certificate, to certfile. The creation of certifi‐ + ;; cates is documented in starttls(8). + (cert opensmtpd-pki-cert + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-pki" "cert" (list file-exists?))))) + (key opensmtpd-pki-key + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-pki" "key" (list file-exists?))))) + ; todo sanitize this. valid parameters are "none", "legacy", or "auto". + (dhe opensmtpd-pki-dhe + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-dhe" "dhe" (list false? string?)))))) + +(define-record-type* + opensmtpd-lmtp make-opensmtpd-lmtp + opensmtpd-lmtp? + (destination opensmtpd-lmtp-destination + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-lmtp" "destination" + (list string?))))) + (rcpt-to opensmtpd-lmtp-rcpt-to + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-lmtp" "rcpt-to" + (list false? string?)))))) + +(define-record-type* + opensmtpd-mda make-opensmtpd-mda + opensmtpd-mda? + (name opensmtpd-mda-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-mda" "name" + (list string?))))) + ;; TODO should I allow this command to be a gexp? + (command opensmtpd-mda-command + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-mda" "command" + (list string?)))))) + +(define-record-type* + opensmtpd-maildir make-opensmtpd-maildir + opensmtpd-maildir? + (pathname opensmtpd-maildir-pathname + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-maildir" "pathname" + (list false? string?))))) + (junk opensmtpd-maildir-junk + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-maildir" "junk" + (list boolean?)))))) + +(define-record-type* + opensmtpd-local-delivery make-opensmtpd-local-delivery + opensmtpd-local-delivery? + (name opensmtpd-local-delivery-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "name" + (list string?))))) + (method opensmtpd-local-delivery-method + (default "mbox") + (sanitize (lambda (var) + (cond + ((or (opensmtpd-lmtp? var) + (opensmtpd-maildir? var) + (opensmtpd-mda? var) + (string=? var "mbox") + (string=? var "expand-only") + (string=? var "forward-only")) + var) + (else + (begin + (display (string-append " fieldname 'method' must be of type \n" + "\"mbox\", \"expand-only\", \"forward-only\" \n" + ", , \n" + "or .\n")) + (throw 'bad! var))))))) + (alias opensmtpd-local-delivery-alias + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "alias" + (list false? opensmtpd-table?))))) + (ttl opensmtpd-local-delivery-ttl + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "ttl" + (list false? string?))))) + (user opensmtpd-local-delivery-user + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "user" + (list false? string?))))) + (userbase opensmtpd-local-delivery-userbase + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "userbase" + (list false? opensmtpd-table?))))) + (virtual opensmtpd-local-delivery-virtual + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "virtual" + (list false? opensmtpd-table?))))) + (wrapper opensmtpd-local-delivery-wrapper + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "wrapper" + (list false? string?)))))) + +;; FIXME/TODO this is a valid opensmtpd-relay record +;; (opensmtpd-relay +;; (pki (opensmtpd-pki +;; (domain "gnucode.me") +;; (cert "opensmtpd.scm") +;; (key "opensmtpd.scm")))) +;; BUT how does it relay the email? What host does it use? +;; I think opensmtpd-relay-configuration needs "method" field. +;; the method field might need to be another record...BUT basically the relay has to have a 'backup', 'backup-mx', +;; or 'domain', or 'host' defined. +(define-record-type* + opensmtpd-relay make-opensmtpd-relay + opensmtpd-relay? + (name opensmtpd-relay-name + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "name" + (list string?)))) + (default #f)) + (backup opensmtpd-relay-backup ;; boolean + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "backup" + (list boolean?))))) + (backup-mx opensmtpd-relay-backup-mx ;; string mx name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "backup-mx" + (list false? string?))))) + (helo opensmtpd-relay-helo + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "helo" + (list false? string? opensmtpd-table?)))) + (default #f)) + (helo-src opensmtpd-relay-helo-src + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "helo-src" + (list false? string? opensmtpd-table?)))) + (default #f)) + (domain opensmtpd-relay-domain + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "domain" + (list false? opensmtpd-table?)))) + (default #f)) + (host opensmtpd-relay-host + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "host" + (list false? string?)))) + (default #f)) + (pki opensmtpd-relay-pki + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "pki" + (list false? opensmtpd-pki?))))) + (srs opensmtpd-relay-srs + (default #f) + (lambda (var) + (my/sanitize var "opensmtpd-relay" "srs" + (list boolean?)))) + (tls opensmtpd-relay-tls + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "tls" + (list false? string?))))) + (auth opensmtpd-relay-auth + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "auth" + (list false? opensmtpd-table?)))) + (default #f)) + (mail-from opensmtpd-relay-mail-from + (default #f)) + ;; string "127.0.0.1" or "" or "
" + ;; TODO should I do some sanitizing to make sure that the string? here is actually an IP address or a valid interface? + (src opensmtpd-relay-src + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "src" + (list false? string? opensmtpd-table?)))) + (default #f))) + +;; this record is used by & +;; +(define-record-type* + opensmtpd-option make-opensmtpd-option + opensmtpd-option? + (option opensmtpd-option-option + (default #f) + (sanitize (lambda (var) + (if (and (string? var) + (or (string-in-list? var (list "fcrdns" "rdns" + "src" "helo" + "auth" "mail-from" + "rcpt-to" + "for" + "for any" "for local" + "for domain" "for rcpt-to" + "from any" "from auth" + "from local" "from mail-from" + "from rdns" "from socket" + "from src" "auth" + "helo" "mail-from" + "rcpt-to" "tag" "tls")))) + + var + (begin + (display (string-append " fieldname: 'option' is of type \n" + "string. The string can be either 'fcrdns', \n" + " 'rdns', 'src', 'helo', 'auth', 'mail-from', or 'rcpt-to', \n" + "'for', 'for any', 'for local', 'for domain', 'for rcpt-to', \n" + "'from any', 'from auth', 'from local', 'from mail-from', 'from rdns', 'from socket', \n" + "'from src', 'auth helo', 'mail-from', 'rcpt-to', 'tag', or 'tls' \n")) + + (throw 'bad! var)))))) + (not opensmtpd-option-not + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-option" "not" + (list boolean?))))) + (regex opensmtpd-option-regex + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-option" "regex" + (list boolean?))))) + (data opensmtpd-option-data + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-option" "data" + (list false? string? opensmtpd-table?)))))) + +(define-record-type* + opensmtpd-filter-phase make-opensmtpd-filter-phase + opensmtpd-filter-phase? + (name opensmtpd-filter-phase-name ;; string chain-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter-phase" "name" + (list string?))))) + (phase opensmtpd-filter-phase-phase ;; string + (default #f) + (sanitize (lambda (var) + (if (and (string? var) + (string-in-list? var (list "connect" + "helo" + "mail-from" + "rcpt-to" + "data" + "commit"))) + var + (begin + (display (string-append " fieldname: 'phase' is of type \n" + "string. The string can be either 'connect'," + " 'helo', 'mail-from', 'rcpt-to', 'data', or 'commit.'\n ")) + + (throw 'bad! var)))))) + (options opensmtpd-filter-phase-options + (default #f) + (sanitize (lambda (var) + ;; returns #t if list is a unique list of + (define (list-of-opensmtpd-option? list) + (and (list-of-type? list opensmtpd-option?) + (not (contains-duplicate? list)))) + + (define (list-has-duplicates-or-non-opensmtpd-option list) + (not (list-of-opensmtpd-option? list))) + + ;; input + ;; return #t if fieldname 'option' + ;; that needs a corresponding table has one. Otherwise #f + (define (opensmtpd-option-has-table? record) + (define decision (opensmtpd-option-option record)) + (and (string? decision) + ;; if option needs a table, check for a table + (if (string-in-list? decision (list "src" + "helo" + "mail-from" + "rcpt-to")) + (opensmtpd-table? (opensmtpd-option-data record)) + #t))) + + (define (list-of-opensmtpd-option-has-table? list) + (list-of-type? list opensmtpd-option-has-table?)) + + (define (some-opensmtpd-option-in-list-lack-table? list) + (not (list-of-opensmtpd-option-has-table? list))) + + (sanitize-options-for-filter-phase-configuration var) + ))) + (decision opensmtpd-filter-phase-decision + (default #f) + (sanitize (lambda (var) + (if (and (string? var) + (string-in-list? var (list "bypass" "disconnect" + "reject" "rewrite" "junk"))) + var + (begin + (display (string-append " fieldname: 'decision' is of type \n" + "string. The string can be either 'bypass'," + " 'disconnect', 'reject', 'rewrite', or 'junk'.\n")) + (throw 'bad! var)))))) + (message opensmtpd-filter-phase-message + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter-phase" "message" + (list false? string?))))) + (value opensmtpd-filter-phase-value + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter-phase" "value" + (list false? number?)))))) + +(define-record-type* + opensmtpd-filter make-opensmtpd-filter + opensmtpd-filter? + (name opensmtpd-filter-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter" "name" + (list string?))))) + (exec opensmtpd-filter-exec + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter" "exec" + (list boolean?))))) + (proc opensmtpd-filter-proc ; a string like "rspamd" or the command to start it like "/path/to/rspamd --option=arg --2nd-option=arg2" + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter" "proc" + (list string? list-of-strings-or-gexps?)))))) + +;; There is another type of filter that opensmtpd supports, which is a filter chain. +;; A filter chain is a list of and . +;; This lets you apply several filters under one filter name. I could have defined +;; a record type for it, but the record would only have had two fields: name and list-of-filters. +;; Why write that as a record? That's too simple. +;; returns #t if list is a unique list of or +;; returns # otherwise +(define (opensmtpd-filter-chain? %filters) + (and (list-of-unique-filter-or-filter-phase? %filters) + (< 1 (length %filters)))) + +(define-record-type* + opensmtpd-interface make-opensmtpd-interface + opensmtpd-interface? + ;; interface may be an IP address, interface group, or domain name + (interface opensmtpd-interface-interface + (default "lo")) + (family opensmtpd-interface-family + (default #f) + (sanitize (lambda (var) + (cond + ((eq? #f var) ;; var == #f + var) + ((and (string? var) + (string-in-list? var (list "inet4" "inet6"))) + var) + (else + (begin + (display " fieldname 'family' must be string \"inet4\" or \"inet6\".\n") + (throw 'bad! var))))))) + (auth opensmtpd-interface-auth + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "auth" + (list boolean? table-whose-data-are-assoc-list?))))) + (auth-optional opensmtpd-interface-auth-optional + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "auth-optional" + (list boolean? + table-whose-data-are-assoc-list?))))) + ;; TODO add a ca entry? + ;; string FIXME/TODO sanitize this to support a gexp. That way way the + ;; includes directive can include my hacky scheme code that I use for opensmtpd-dkimsign. + (filters opensmtpd-interface-filters + (default #f) + (sanitize (lambda (var) + (sanitize-filter-phases var)))) + (hostname opensmtpd-interface-hostname + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "hostname" + (list false? string?))))) + (hostnames opensmtpd-interface-hostnames + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "hostnames" + (list false? table-whose-data-are-assoc-list?))))) + (mask-src opensmtpd-interface-mask-src + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "mask-src" + (list boolean?))))) + (disable-dsn opensmtpd-interface-disable-dsn + (default #f)) + (pki opensmtpd-interface-pki + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "pki" + (list false? opensmtpd-pki?))))) + (port opensmtpd-interface-port + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "port" + (list false? integer?))))) + (proxy-v2 opensmtpd-interface-proxy-k2 + (default #f)) + (received-auth opensmtpd-interface-received-auth + (default #f)) + ;; TODO add in a senders option! + ;; string or record + ;; (senders opensmtpd-interface-senders + ;; (sanitize (lambda (var) + ;; (my/sanitize var "opensmtpd-interface" "port" (list false? integer?)))) + ;; (default #f)) + (secure-connection opensmtpd-interface-secure-connection + (default #f) + (sanitize (lambda (var) + (cond ((boolean? var) + var) + ((and (string? var) + (string-in-list? var + (list "smtps" "tls" + "tls-require" + "tls-require-verify"))) + var) + (else + (begin + (display (string-append " fieldname 'secure-connection' can be \n" + "one of the following strings: \n'smtps', 'tls', 'tls-require', \n" + "or 'tls-require-verify'.\n")) + (throw 'bad! var))))))) + (tag opensmtpd-interface-tag + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "tag" + (list false? string?)))) + (default #f))) + +(define-record-type* + opensmtpd-socket-configuration make-opensmtpd-socket-configuration + opensmtpd-socket-configuration? + ;; false or or list of + (filters opensmtpd-socket-configuration-filters + (sanitize (lambda (var) + (sanitize-filter-phases var))) + (default #f)) + (mask-src opensmtpd-socket-configuration-mask-src + (default #f)) + (tag opensmtpd-socket-configuration-tag + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "tag" + (list false? string?)))) + (default #f))) + + +(define-record-type* + opensmtpd-match make-opensmtpd-match + opensmtpd-match? + ;;TODO? Perhaps I should add in a reject fieldname. If reject + ;;is #t, then the match record will be a reject match record. + ;; (opensmtpd-match (reject #t)) vs. (opensmtpd-match (action 'reject)) + ;; To do this, I will also have to 'reject' mutually exclusive. AND an match with 'reject' can have no action defined. + (action opensmtpd-match-action + (default #f) + (sanitize (lambda (var) + (if (or (opensmtpd-relay? var) + (opensmtpd-local-delivery? var) + (eq? (quote reject) var)) + var + (begin + (display + (string-append " fieldname 'action' is of type , \n" + ", or (quote reject).\n" + "If its var is (quote reject), then the match rejects the incoming message\n" + "during the SMTP dialogue.\n")) + (throw 'bad! var)))))) + (options opensmtpd-match-options + (default #f) + (sanitize (lambda (var) + (cond ((not var) + #f) + ((not (list-of-unique-opensmtpd-option? var)) + (throw-error var '(" fieldname 'options' is a list of unique \n" + " records. \n"))) + (else (sanitize-list-of-options-for-match-configuration var))))))) + +(define-record-type* + opensmtpd-smtp make-opensmtpd-smtp + opensmtpd-smtp? + (ciphers opensmtpd-smtp-ciphers + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp" "ciphers" + (list false? string?))))) + (limit-max-mails opensmtpd-smtp-limit-max-mails + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp" "limit-max-mails" + (list false? integer?))))) + (limit-max-rcpt opensmtpd-smtp-limit-max-rcpt + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp" "limit-max-rcpt" + (list false? integer?))))) + (max-message-size opensmtpd-smtp-max-message-size + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp" "max-message-size" + (list false? integer? string?))))) + ;; FIXME/TODO the sanitize function of sub-addr-delim should accept a string of length one not string? + (sub-addr-delim opensmtpd-smtp-sub-addr-delim + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp" "sub-addr-delim" + (list false? integer? string?)))))) + +(define-record-type* + opensmtpd-srs make-opensmtpd-srs + opensmtpd-srs? + ;; TODO should this be a file? + (key opensmtpd-srs-key + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-srs" "key" + (list false? boolean? string?))))) + ;; TODO should this also be a file? + (backup-key opensmtpd-srs-backup-key + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-srs" "backup-key" + (list false? integer?))))) + (ttl-delay opensmtpd-srs-ttl-delay + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-srs" "ttl-delay" + (list false? string?)))))) + +(define-record-type* + opensmtpd-queue make-opensmtpd-queue + opensmtpd-queue? + (compression opensmtpd-queue-compression + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-queue" "compression" + (list boolean?))))) + (encryption opensmtpd-queue-encryption + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-queue" "encryption" + (list boolean? string? file-exists?))))) + (ttl-delay opensmtpd-queue-ttl-delay + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-queue" "ttl-delay" + (list false? string?)))))) + (define-record-type* opensmtpd-configuration make-opensmtpd-configuration opensmtpd-configuration? - (package opensmtpd-configuration-package - (default opensmtpd)) + (package opensmtpd-configuration-package + (default opensmtpd)) (config-file opensmtpd-configuration-config-file - (default %default-opensmtpd-config-file)) + (default #f)) + ;; FIXME/TODO should I include a admd authservid entry? + + ;; TODO sanitize this properly with perhaps a . + (bounce opensmtpd-configuration-bounce + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "bounce" + (list false? list?))))) + (cas opensmtpd-configuration-cas + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "cas" + (list false? list-of-opensmtpd-ca?))))) + ;; list of many records of type opensmtpd-interface + (listen-ons opensmtpd-configuration-listen-ons + (default (list (opensmtpd-interface))) + (sanitize (lambda (var) + (if (list-of-opensmtpd-interface? var) + var + (begin + (display " fieldname 'listen-ons' expects a list of records ") + (display "of one or more unique records.\n") + (throw 'bad! var)))))) + ;; accepts type + (listen-on-socket opensmtpd-configuration-listen-on-socket + (default (opensmtpd-socket-configuration))) + (includes opensmtpd-configuration-includes ;; list of strings of absolute path names + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "includes" + (list false? list-of-strings? gexp?))))) + (matches opensmtpd-configuration-matches + (default (list (opensmtpd-match + (action (opensmtpd-local-delivery + (name "local") + (method "mbox"))) + (options (list + (opensmtpd-option + (option "for local"))))) + (opensmtpd-match + (action (opensmtpd-relay + (name "outbound"))) + (options (list + (opensmtpd-option + (option "from local")) + (opensmtpd-option + (option "for any"))))))) + ;; TODO perhaps I should sanitize this function like I sanitized the 'filters'. + ;; I definitely should sanitize this function a bit more. For example, you could have two different + ;; actions, one for local delivery and one for remote, with the same name. I should make sure that + ;; I have no two different actions with the same name. + (sanitize (lambda (var) + ;; Should we do more sanitizing here? eg: "from socket" should NOT have a table or value + var + (my/sanitize var "opensmtpd-configuration" "matches" + (list list-of-unique-opensmtpd-match?))))) + ;; list of many records of type mda-wrapper + ;; TODO/FIXME support using gexps here + ;; eg (list "name" gexp) + (mda-wrappers opensmtpd-configuration-mda-wrappers + (default #f) + (sanitize (lambda (var) + (my/sanitize var + "opensmtpd-configuration" + "mda-wrappers" + (list false? string?))))) + (mta-max-deferred opensmtpd-configuration-mta-max-deferred + (default 100) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "mta-max-deferred" + (list number?))))) + + ;; TODO should I add a fieldname proc _proc-name_ _command_ as found in the man 5 smtpd.conf ? + + (queue opensmtpd-configuration-queue + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "queue" + (list false? opensmtpd-queue?))))) + (smtp opensmtpd-configuration-smtp + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "smtp" + (list false? opensmtpd-smtp?))))) + (srs opensmtpd-configuration-srs + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "srs" + (list false? opensmtpd-srs?))))) (setgid-commands? opensmtpd-setgid-commands? (default #t))) +;; this help procedure is used 3 or 4 times by sanitize-list-of-options-for-match-configuration +(define* (throw-error-duplicate-option option error-arg #:key (record-name "match")) + (throw-error error-arg + (list (string-append "'s fieldname 'options' has two\n") + (string-append " records with fieldname 'option' with value '" option "'. \n") + (string-append "You can only have one option with value '" option "' in the options list.\n")))) + +;; this procedure sanitizes the fieldname opensmtpd-match-options +(define* (sanitize-list-of-options-for-match-configuration %options) + (let loop ((%traversing-options %options) + ;; sanitized-options is an alist that may end of looking like: + ;; (("for" (opensmtpd-option (option "for any"))) + ;; ("from" (opensmtpd-option (option "from any")))) + (%sanitized-options '())) + (if (null? %traversing-options) + (remove false? + (list + (assoc-ref %sanitized-options "for") + (assoc-ref %sanitized-options "from") + (assoc-ref %sanitized-options "auth") + (assoc-ref %sanitized-options "helo") + (assoc-ref %sanitized-options "mail-from") + (assoc-ref %sanitized-options "rcpt-to") + (assoc-ref %sanitized-options "tag") + (assoc-ref %sanitized-options "tls"))) + (let* ((option-record (car %traversing-options)) + (option-string (opensmtpd-option-option option-record))) + (cond ((string=? "auth" option-string) + (if (assoc-ref %sanitized-options "auth") + (throw-error-duplicate-option "auth" %traversing-options) + (loop (cdr %traversing-options) (alist-cons "auth" option-record %sanitized-options)))) + ((string=? "helo" option-string) + (cond [(assoc-ref %sanitized-options "helo") + (throw-error-duplicate-option "helo" %traversing-options)] + [(not (opensmtpd-option-data option-record)) + (throw-error option-record + (list " with fieldname 'option' with value 'helo' \n" + "must have a 'data' of type string or .\n"))] + [else (loop (cdr %traversing-options) (alist-cons "helo" option-record %sanitized-options))])) + ((string=? "mail-from" option-string) + (cond ((assoc-ref %sanitized-options "mail-from") + (throw-error-duplicate-option "mail-from" %traversing-options)) + ((not (opensmtpd-option-data option-record)) + (throw-error option-record + (list " with fieldname 'option' with value 'mail-from' \n" + "must have a 'data' of type string or .\n"))) + (else (loop (cdr %traversing-options) (alist-cons "mail-from" option-record %sanitized-options))))) + ((string=? "rcpt-to" option-string) + (cond [(assoc-ref %sanitized-options "rcpt-to") + (throw-error-duplicate-option "rcpt-to" %traversing-options)] + [(not (opensmtpd-option-data option-record)) + (throw-error option-record + (list " with fieldname 'option' with value 'rcpt-to' \n" + "must have a 'data' of type string or .\n"))] + [else (loop (cdr %traversing-options) (alist-cons "rcpt-to" option-record %sanitized-options))])) + ((string=? "tag" option-string) + (cond ((assoc-ref %sanitized-options "tag") + (throw-error-duplicate-option "tag" %traversing-options)) + ((not (string? (opensmtpd-option-data option-record))) + (throw-error option-record + (list " with fieldname 'option' with value 'tag' \n" + "must have a 'data' of type string.\n"))) + (else (loop (cdr %traversing-options) (alist-cons "tag" option-record %sanitized-options))))) + ((string=? "tls" option-string) + (cond [(assoc-ref %sanitized-options "tls") + (throw-error-duplicate-option "tls" %traversing-options)] + [(or (opensmtpd-option-data option-record) + (opensmtpd-option-regex option-record)) + (throw-error option-record + (list " with fieldname 'option' with value 'tls', then \n" + "fieldname 'data' cannot be defined.\n"))] + [else (loop (cdr %traversing-options) (alist-cons "tls" option-record %sanitized-options))])) + ((string=? "for" (substring option-string 0 3)) + (cond ((assoc-ref %sanitized-options "for") + (throw-error %options + `("'s fieldname 'options' can only have one 'for' option. \n" + "But '" ,option-string "' and '" + ,(opensmtpd-option-option (assoc-ref %sanitized-options "for")) "' are present.\n"))) + ((and (string-in-list? option-string (list "for any" "for local")) ; for any cannot have a data field. + (or (opensmtpd-option-data option-record) + (opensmtpd-option-regex option-record))) + (throw-error option-record + (list "When 's fieldname 'options' value is 'for any' \n" + "or 'for local', then its 'data' and 'regex' field must be #f. \n"))) + ((and (string-in-list? option-string (list "for domain" "for rcpt-to")) ; for domain must have a data field. + (not (opensmtpd-option-data option-record))) + (throw-error option-record + (list "When 's fieldname 'options' value is 'for domain' \n" + "or 'for rcpt-to', then its 'data' field must be a string or an \n" + " record.\n"))) + (else (loop (cdr %traversing-options) (alist-cons "for" option-record %sanitized-options))))) + ((string=? "from" (substring option-string 0 4)) + (cond ((assoc-ref %sanitized-options "from") + (throw-error %options + `("'s fieldname 'options' can only have one 'from' option. \n" + "But '" ,option-string "' and '" + ,(opensmtpd-option-option (assoc-ref %sanitized-options "from")) "' are present.\n"))) + ((and (string-in-list? option-string (list "from any" "from local" "from socket")) ; for any cannot have a data field. + (or (opensmtpd-option-data option-record) + (opensmtpd-option-regex option-record))) + (throw-error option-record + (list "When 's fieldname 'options' value is 'from any', \n" + " 'from local', or 'from socket', then its 'data' and 'regex' field must be #f. \n"))) + ((and (string-in-list? option-string (list "from mail-from" "from src")) ; for domain must have a data field. + (not (opensmtpd-option-data option-record))) + (throw-error option-record + (list "When 's fieldname 'options' value is 'from mail-from' \n" + "or 'from src', then its 'data' field must be a string or an \n" + " record.\n"))) + (else (loop (cdr %traversing-options) (alist-cons "from" option-record %sanitized-options)))))))))) + +;; if the list of filters in opensmtpd-interface-filters +;; and in opensmtpd-socket-configuration-filters has two +;; filters with the same name, this will return #t +;; otherwise false +(define (duplicate-filter-name? %filters) + (contains-duplicate? + (let loop ((%filters %filters)) + (if (null? %filters) + '() + (cond + ((opensmtpd-filter-phase? (car %filters)) + (cons (opensmtpd-filter-phase-name (car %filters)) + (loop (cdr %filters)))) + (else + (cons (opensmtpd-filter-name (car %filters)) + (loop (cdr %filters))))))))) + +(define (list-has-duplicates-or-non-filters? list) + (not (list-of-unique-filter-or-filter-phase? list))) + +(define (filter-phase-has-message-and-value? record) + (and (opensmtpd-filter-phase-message record) + (opensmtpd-filter-phase-value record))) + +;; return #t if phase needs a message. Or if the message did not start with a 4xx or 5xx status code. +;; otherwise #f +(define (filter-phase-decision-lacks-proper-message? record) + (define decision (opensmtpd-filter-phase-decision record)) + (if (string-in-list? decision (list "disconnect" "reject")) + ;; this message needs to be RFC compliant, meaning + ;; that it need to start with 4xx or 5xx status code + (cond ((eq? #f (opensmtpd-filter-phase-message record)) + #t) + ((string? (opensmtpd-filter-phase-message record)) + (let ((number (string->number + (substring + (opensmtpd-filter-phase-message record) 0 3)))) + (if (and (number? number) + (and (< number 600) (> number 399))) + #f + #t)))) + #f)) + +;; 'decision' "rewrite" requires 'value' to be a number. +(define (filter-phase-lacks-proper-value? record) + (define decision (opensmtpd-filter-phase-decision record)) + (if (string=? "rewrite" decision) + (if (and (number? (opensmtpd-filter-phase-value record)) + (eq? #f (opensmtpd-filter-phase-message record))) + #f + #t) + #f)) + +;; 'decision' "junk" or "bypass" cannot have a message or a value. +(define (filter-phase-has-incorrect-junk-or-bypass? record) + (and + (string-in-list? + (opensmtpd-filter-phase-decision record) + (list "junk" "bypass")) + (or + (opensmtpd-filter-phase-value record) + (opensmtpd-filter-phase-message record)))) + +(define (filter-phase-junks-after-commit? record) + (and (string=? (opensmtpd-filter-phase-decision record) "junk") + (string=? (opensmtpd-filter-phase-phase record) "commit"))) + +;; returns #t if list is a unique list of or +;; returns # otherwise +(define (list-of-unique-filter-or-filter-phase? %filters) + (and (list? %filters) + (not (null? %filters)) + ;; this list is made up of only or + (primitive-eval + (cons 'and (map (lambda (filter) + (or (opensmtpd-filter? filter) + (opensmtpd-filter-phase? filter))) + %filters))) + (not (contains-duplicate? %filters)))) + +;; the sanitize procedures used for sanitizing and +;; fieldname 'filters'. +;; It primarily sanitizes . The only sanitization it does +;; for s, is no make sure there are no duplicate filter names. +(define (sanitize-filter-phases %list) + ;; the order of the first two tests in this cond is important. + ;; (false?) has to be 1st and (list-has-duplicates-or-non-filters?) has to be second. + ;; You may optionally re-order the other alternates in the cond. + (cond ((false? %list) + #f) + ((list-has-duplicates-or-non-filters? %list) + (begin + (display (string-append " fieldname: 'filters' is a list, in which each unique element \n" + "is of type or .\n")) + (throw 'bad! %list))) + ((duplicate-filter-name? %list) + (throw-error %list (list "has a duplicate filter name.\n") + #:record-name "interface" + #:fieldname "filters")) + (else + (let loop ([%traversing-list %list] + [%original-list %list]) + (if (null? %traversing-list) + %original-list + (cond [(opensmtpd-filter? (car %traversing-list)) + (loop (cdr %traversing-list) %original-list)] + [(filter-phase-has-message-and-value? (car %traversing-list)) + (begin + (display (string-append " cannot have defined fieldnames 'value' \n" + "and 'message'.\n")) + (throw 'bad! (car %traversing-list)))] + [(filter-phase-decision-lacks-proper-message? (car %traversing-list)) + (begin + (display (string-append " fieldname: 'decision' options \n" + "\"disconnect\" and \"reject\" require fieldname 'message' to have an RFC \n" + "compliant string, which means that the string must begin with a 4xx or 5xx status code.\n")) + (throw 'bad! (car %traversing-list)))] + [(filter-phase-lacks-proper-value? (car %traversing-list)) + (begin + (display (string-append " fieldname: 'decision' option \n" + "\"rewrite\" requires fieldname 'value' to have a number.\n")) + (throw 'bad! (car %traversing-list)))] + [(filter-phase-has-incorrect-junk-or-bypass? (car %traversing-list)) + (begin + (display (string-append " fieldname 'decision' option \n" + "\"junk\" or 'bypass' cannot have a defined fieldnames 'message' or 'value'.\n")) + (throw 'bad! (car %traversing-list)))] + [(filter-phase-junks-after-commit? (car %traversing-list)) + (begin + (display (string-append " fieldname 'decision' option \n" + "\"junk\" cannot junk an email during 'phase' \"commit\".\n")) + (throw 'bad! (car %traversing-list)))] + [else (loop (cdr %traversing-list) %original-list)])))))) + +(define* (sanitize-options-for-filter-phase-configuration %options) + (if (false? %options) + (throw-error #f + (list "must have at least one opensmtpd-option record.") + #:record-name "filter-phase" + #:fieldname "options") + (let loop ((%traversing-options %options) + ;; sanitized-options is an alist that may end of looking like: + ;; (("for" (opensmtpd-option (option "for any"))) + ;; ("from" (opensmtpd-option (option "from any")))) + (%sanitized-options '())) + (if (null? %traversing-options) + (remove false? + (list + (assoc-ref %sanitized-options "fcrdns") + (assoc-ref %sanitized-options "rdns") + (assoc-ref %sanitized-options "src") + (assoc-ref %sanitized-options "helo") + (assoc-ref %sanitized-options "auth") + (assoc-ref %sanitized-options "mail-from") + (assoc-ref %sanitized-options "rcpt-to"))) + (let* ((option-record (car %traversing-options)) + (option-string (opensmtpd-option-option option-record))) + (cond ((assoc-ref %sanitized-options option-string) + ;; if we see two "rdns" (for example), throw a "duplicate + ;; option" error. + (throw-error-duplicate-option option-string option-record + #:record-name "filter-phase")) + ;; the next 4 options must have fieldname 'data' defined. + ((or (string=? option-string "src") + (string=? option-string "helo") + (string=? option-string "mail-from") + (string=? option-string "rcpt-to")) + (if (not (opensmtpd-table? + (opensmtpd-option-data option-record))) + (throw-error option-record (list "must have fieldname 'data' defined.\n") + #:record-name "option" + #:fieldname option-string) + (loop (cdr %traversing-options) + (alist-cons option-string option-record %sanitized-options)))) + ;;fcrdns cannot have fieldname data defined + ((string=? "fcrdns" option-string) + (if (opensmtpd-option-data option-record) + (throw-error option-record (list "cannot have fieldname data defined.\n") + #:record-name "option" + #:fieldname "rdns") + (loop (cdr %traversing-options) + (alist-cons "fcrdns" option-record %sanitized-options)))) + ;; rdns and auth cannot be made invalidly; skip testing them. + ((or (string=? "rdns" option-string) + (string=? "auth" option-string)) + (loop (cdr %traversing-options) + (alist-cons "auth" option-record + %sanitized-options))) + (else (throw-error option-record + (list "has an invalid option name.") + #:record-name "filter-phase" + #:fieldname option-string)))))))) + +(define* (throw-error var %strings + #:key + (record-name #f) + (fieldname #f)) + (if (and record-name fieldname) + (begin + (display (string-append " fieldname " fieldname " " + (apply string-append %strings))) + (throw 'bad! var)) + (begin + (display (apply string-append %strings)) + (throw 'bad! var)))) + +;; this is used for sanitizing fieldname 'options' +(define (contains-duplicate? list) + (if (null? list) + #f + (or + ;; check if (car list) is in (cdr list) + (primitive-eval (cons 'or + (map (lambda (var) (equal? var (car list))) + (cdr list)))) + ;; check if (cdr list) contains duplicate + (contains-duplicate? (cdr list))))) + +;; given a list and procedure, this tests that each element of list is of type +;; ie: (list-of-type? list string?) tests each list is of type string. +(define (list-of-type? list proc?) + (if (and (list? list) + (not (null? list))) + (let loop ((list list)) + (if (null? list) + #t + (if (proc? (car list)) + (loop (cdr list)) + #f))) + #f)) + +(define (list-of-strings? list) + (list-of-type? list string?)) + +(define (list-of-unique-opensmtpd-option? list) + (and (list-of-type? + list opensmtpd-option?) + (not (contains-duplicate? list)))) + +(define (list-of-opensmtpd-ca? list) + (list-of-type? list opensmtpd-ca?)) + +(define (list-of-opensmtpd-pki? list) + (list-of-type? list opensmtpd-pki?)) + +(define (list-of-opensmtpd-interface? list) + (and (list-of-type? list opensmtpd-interface?) + (not (contains-duplicate? list)))) + +(define (list-of-unique-opensmtpd-match? list) + (and (list-of-type? list opensmtpd-match?) + (not (contains-duplicate? list)))) + +(define* (list-of-strings->string list + #:key + (string-delimiter ", ") + (postpend "") + (append "") + (drop-right-number 2)) + (string-drop-right + (string-append (let loop ((list list)) + (if (null? list) + "" + (string-append append (car list) postpend + string-delimiter + (loop (cdr list))))) + append) + drop-right-number)) + +;; at the moment I cannot define this by using list-of-type? +;; the first (not (null? assoc-list)) prevents that. +(define (assoc-list? assoc-list) + (list-of-type? assoc-list (lambda (pair) + (if (and (pair? pair) + (string? (car pair)) + (string? (cdr pair))) + #t + #f)))) + +(define* (variable->string var #:key (append "") (postpend " ")) + (let ((var (if (number? var) + (number->string var) + var))) + (if var + (string-append append var postpend) + ""))) + +;; this procedure takes in one argument. +;; if that argument is an whose fieldname 'values' is an assoc-list, then it returns +;; #t, #f if otherwise. +;; TODO should I remove these two functions? And instead use the (opensmtpd-table-type) procedure? +(define (table-whose-data-are-assoc-list? table) + (if (not (opensmtpd-table? table)) + #f + (assoc-list? (opensmtpd-table-data table)))) + +;; this procedure takes in one argument +;; if that argument is an whose fieldname 'values' is a list of strings, then it returns +;; #t, #f if otherwise. +(define (table-whose-data-are-a-list-of-strings? table) + (if (not (opensmtpd-table? table)) + #f + (list-of-strings? (opensmtpd-table-data table)))) + +;; these next few functions help me to turn
s +;; into strings suitable to fit into "opensmtpd.conf". +(define (assoc-list->string assoc-list) + (string-drop-right + (let loop ((assoc-list assoc-list)) + (if (null? assoc-list) + "" + ;; pair is (cons "hello" "world") -> ("hello" . "world") + (let ((pair (car assoc-list))) + (string-append + "\"" (car pair) "\"" + " = " + "\"" (cdr pair) "\"" + ", " + (loop (cdr assoc-list)))))) + 2)) + +;; The following functions convert various records into strings. +;; +;; can be of type: (quote list-of-strings) or (quote assoc-list) +(define (opensmtpd-table->string table) + (string-append "table " (opensmtpd-table-name table) " " + (let ((type (opensmtpd-table-type table))) + (cond ((eq? type (quote list-of-strings)) + (string-append "{ " (list-of-strings->string (opensmtpd-table-data table) + #:append "\"" + #:drop-right-number 3 + #:postpend "\"") " }")) + ((eq? type (quote assoc-list)) + (string-append "{ " (assoc-list->string (opensmtpd-table-data table)) " }")) + ((eq? type (quote db)) + (string-append "db:" (opensmtpd-table-data table))) + ((eq? type (quote file)) + (string-append "file:" (opensmtpd-table-data table))) + (else (throw 'youMessedUp table)))) + " \n")) + +(define (opensmtpd-interface->string record) + (string-append "listen on " + (opensmtpd-interface-interface record) " " + (let* ((hostname (opensmtpd-interface-hostname record)) + (hostnames (if (opensmtpd-interface-hostnames record) + (opensmtpd-table-name (opensmtpd-interface-hostnames record)) + #f)) + (filters (opensmtpd-interface-filters record)) + (filter-name (if filters + (if (< 1 (length filters)) + (generate-filter-chain-name filters) + (if (opensmtpd-filter? (car filters)) + (opensmtpd-filter-name (car filters)) + (opensmtpd-filter-phase-name (car filters)))) + #f)) + (mask-src (opensmtpd-interface-mask-src record)) + (tag (opensmtpd-interface-tag record)) + (secure-connection (opensmtpd-interface-secure-connection record)) + (port (opensmtpd-interface-port record)) + (pki (opensmtpd-interface-pki record)) + (auth (opensmtpd-interface-auth record)) + (auth-optional (opensmtpd-interface-auth-optional record))) + (string-append + (if mask-src + (string-append "mask-src ") + "") + (variable->string hostname #:append "hostname ") + (variable->string hostnames #:append "hostnames <" #:postpend "> ") + (variable->string filter-name #:append "filter \"" #:postpend "\" ") + (variable->string tag #:append "tag \"" #:postpend "\" ") + (if secure-connection + (cond ((string=? "smtps" secure-connection) + "smtps ") + ((string=? "tls" secure-connection) + "tls ") + ((string=? "tls-require" secure-connection) + "tls-require ") + ((string=? "tls-require-verify" secure-connection) + "tls-require verify ")) + "") + (variable->string port #:append "port " #:postpend " ") + (if pki + (variable->string (opensmtpd-pki-domain pki) #:append "pki ") + "") + (if auth + (string-append "auth " + (if (opensmtpd-table? auth) + (string-append "<" (opensmtpd-table-name auth) "> ") + "")) + "") + (if auth-optional + (string-append "auth-optional " + (if (opensmtpd-table? auth-optional) + (string-append "<" (opensmtpd-table-name auth-optional) "> ") + "")) + "") + "\n")))) + +(define (opensmtpd-socket->string record) + (string-append "listen on socket " + (let* ((filters (opensmtpd-socket-configuration-filters record)) + (filter-name (if filters + (if (< 1 (length filters)) + (generate-filter-chain-name filters) + (if (opensmtpd-filter? (car filters)) + (opensmtpd-filter-name (car filters)) + (opensmtpd-filter-phase-name (car filters)))) + #f)) + (mask-src (opensmtpd-socket-configuration-mask-src record)) + (tag (opensmtpd-socket-configuration-tag record))) + (string-append + (if mask-src + (string-append "mask-src ") + "") + (variable->string filter-name #:append "filter \"" #:postpend "\" ") + (variable->string tag #:append "tag \"" #:postpend "\" ") + "\n")))) + +(define (opensmtpd-relay->string record) + (let ((backup (opensmtpd-relay-backup record)) + (backup-mx (opensmtpd-relay-backup-mx record)) + (helo (opensmtpd-relay-helo record)) + ;; helo-src can either be a string IP address or an + (helo-src (if (opensmtpd-relay-helo-src record) + (if (string? (opensmtpd-relay-helo-src record)) + (opensmtpd-relay-helo-src record) + (string-append "<\"" + (opensmtpd-table-name + (opensmtpd-relay-src record)) + "\">")) + #f)) + (domain (if (opensmtpd-relay-domain record) + (opensmtpd-table-name + (opensmtpd-relay-domain record)) + #f)) + (host (opensmtpd-relay-host record)) + (name (opensmtpd-relay-name record)) + (pki (if (opensmtpd-relay-pki record) + (opensmtpd-pki-domain (opensmtpd-relay-pki record)) + #f)) + (srs (opensmtpd-relay-srs record)) + (tls (opensmtpd-relay-tls record)) + (auth (if (opensmtpd-relay-auth record) + (opensmtpd-table-name + (opensmtpd-relay-auth record)) + #f)) + (mail-from (opensmtpd-relay-mail-from record)) + ;; src can either be a string IP address or an + (src (if (opensmtpd-relay-src record) + (if (string? (opensmtpd-relay-src record)) + (opensmtpd-relay-src record) + (string-append "<\"" + (opensmtpd-table-name + (opensmtpd-relay-src record)) + "\">")) + #f))) + + (string-append + "\"" + name + "\" " "relay " + ;;FIXME should I always quote the host fieldname? do I need to quote localhost via "localhost" ? + (variable->string host #:append "host \"" #:postpend "\" ") + (variable->string backup) + (variable->string backup-mx #:append "backup mx ") + (variable->string helo #:append "helo ") + (variable->string helo-src #:append "helo-src ") + (variable->string domain #:append "domain <\"" #:postpend "\"> ") + (variable->string host #:append "host ") + (variable->string pki #:append "pki ") + (variable->string srs) + (variable->string tls #:append "tls ") + (variable->string auth #:append "auth <" #:postpend "> ") + (variable->string mail-from #:append "mail-from ") + (variable->string src #:append "src ") + "\n"))) + +(define (opensmtpd-lmtp->string record) + (string-append "lmtp " + (opensmtpd-lmtp-destination record) + (if (opensmtpd-lmtp-rcpt-to record) + (begin + " " (opensmtpd-lmtp-rcpt-to record)) + ""))) + +(define (opensmtpd-mda->string record) + (string-append "mda " + (opensmtpd-mda-command record) " ")) + +(define (opensmtpd-maildir->string record) + (string-append "maildir " + "\"" + (if (opensmtpd-maildir-pathname record) + (opensmtpd-maildir-pathname record) + "~/Maildir") + "\"" + (if (opensmtpd-maildir-junk record) + " junk " + " "))) + +(define (opensmtpd-local-delivery->string record) + (let ((name (opensmtpd-local-delivery-name record)) + (method (opensmtpd-local-delivery-method record)) + (alias (if (opensmtpd-local-delivery-alias record) + (opensmtpd-table-name + (opensmtpd-local-delivery-alias record)) + #f)) + (ttl (opensmtpd-local-delivery-ttl record)) + (user (opensmtpd-local-delivery-user record)) + (userbase (if (opensmtpd-local-delivery-userbase record) + (opensmtpd-table-name + (opensmtpd-local-delivery-userbase record)) + #f)) + (virtual (if (opensmtpd-local-delivery-virtual record) + (opensmtpd-table-name + (opensmtpd-local-delivery-virtual record)) + #f)) + (wrapper (opensmtpd-local-delivery-wrapper record))) + (string-append + "\"" name "\" " + (cond ((string? method) + (string-append method " ")) + ((opensmtpd-mda? method) + (opensmtpd-mda->string method)) + ((opensmtpd-lmtp? method) + (opensmtpd-lmtp->string method)) + ((opensmtpd-maildir? method) + (opensmtpd-maildir->string method))) + ;; FIXME/TODO support specifying alias file:/path/to/alias-file ? + ;; I do not think that is something that I can do... + (variable->string alias #:append "alias <\"" #:postpend "\"> ") + (variable->string ttl #:append "ttl ") + (variable->string user #:append "user ") + (variable->string userbase #:append "userbase <\"" #:postpend "\"> ") + (variable->string virtual #:append "virtual <" #:postpend "> ") + (variable->string wrapper #:append "wrapper ")))) + +;; this function turns both opensmtpd-local-delivery and +;; opensmtpd-relay into strings. +(define (opensmtpd-action->string record) + (string-append "action " + (cond ((opensmtpd-local-delivery? record) + (opensmtpd-local-delivery->string record)) + ((opensmtpd-relay? record) + (opensmtpd-relay->string record))) + " \n")) + +;; this turns option records found in into strings. +(define* (opensmtpd-option->string record + #:key + (space-after-! #f)) + (let ((not (opensmtpd-option-not record)) + (option (opensmtpd-option-option record)) + (regex (opensmtpd-option-regex record)) + (data (opensmtpd-option-data record))) + (string-append + (if not + (if space-after-! + "! " + "!") + "") + option " " + (if regex + "regex " + "") + (if data + (if (opensmtpd-table? data) + (string-append "<" (opensmtpd-table-name data) "> ") + (string-append data " ")) + "")))) + +(define (opensmtpd-match->string record) + (string-append "match " + (let* ((action (opensmtpd-match-action record)) + (name (cond [(opensmtpd-relay? action) + (opensmtpd-relay-name action)] + [(opensmtpd-local-delivery? action) + (opensmtpd-local-delivery-name action)] + [else 'reject])) + (options (opensmtpd-match-options record))) + (string-append + (if options + (apply string-append + (map opensmtpd-option->string options)) + "") + (if (string? name) + (string-append "action " "\"" name "\" ") + "reject ") + "\n")))) + +(define (opensmtpd-ca->string record) + (string-append "ca " (opensmtpd-ca-name record) " " + "cert \"" (opensmtpd-ca-file record) "\"\n")) + +(define (opensmtpd-pki->string record) + (let ((domain (opensmtpd-pki-domain record)) + (cert (opensmtpd-pki-cert record)) + (key (opensmtpd-pki-key record)) + (dhe (opensmtpd-pki-dhe record))) + (string-append "pki " domain " " "cert \"" cert "\" \n" + "pki " domain " " "key \"" key "\" \n" + (if dhe + (string-append + "pki " domain " " "dhe " dhe "\n") + "")))) + +(define (generate-filter-chain-name list-of-filters) + (string-drop-right (apply string-append + (flatten + (map (lambda (filter) + (list + (if (opensmtpd-filter? filter) + (opensmtpd-filter-name filter) + (opensmtpd-filter-phase-name filter)) + "-")) + list-of-filters))) + 1)) + +;; this procedure takes in a list of and , +;; returns a string of the form: +;; filter "uniquelyGeneratedName" chain chain { "filter-name", "filter-name2" [, ...]} +(define (opensmtpd-filter-chain->string list-of-filters) + (string-append "filter \"" + (generate-filter-chain-name list-of-filters) + "\" " + "chain {" + (string-drop-right + (apply string-append + (flatten + (map (lambda (filter) + (list + "\"" + (if (opensmtpd-filter? filter) + (opensmtpd-filter-name filter) + (opensmtpd-filter-phase-name filter)) + "\", ")) + list-of-filters))) + 2) + "}\n")) + +(define (opensmtpd-filter-phase->string record) + (let ((name (opensmtpd-filter-phase-name record)) + (phase (opensmtpd-filter-phase-phase record)) + (decision (opensmtpd-filter-phase-decision record)) + (options (opensmtpd-filter-phase-options record)) + (message (opensmtpd-filter-phase-message record)) + (value (opensmtpd-filter-phase-value record))) + (string-append "filter " + "\"" name "\" " + "phase " phase " " + "match " + (apply string-append ; turn the options into a string + (flatten + (map (lambda (option) + (opensmtpd-option->string option #:space-after-! #f)) + options))) + " " + decision " " + (if (string-in-list? decision (list "reject" "disconnect")) + (string-append "\"" message "\"") + "") + (if (string=? "rewrite" decision) + (string-append "rewrite " (number->string value)) + "") + "\n"))) + +;; filters elements may be , , +;; and lists that look like (list (opensmtpd-filter...) (opensmtpd-filter-phase ...) +;; ...) +;; this function converts it to a string. +;; Consider if a user passed in a valid , whose total valid filters +;; so that (get-opensmtpd-filters (opensmtpd-configuration)) returns +;; look like this: (we will call this list "total filters"): +;; (list (opensmtpd-filter +;; (name "rspamd") +;; (proc "rspamd")) +;; (list (opensmtpd-filter-phase ; this is a listen-on, with a filter-chain. +;; (name "dkimsign") +;; ...) +;; (opensmtpd-filter +;; (name "rspamd") +;; (proc "rspamd")))) +;; +;; did you notice that filter "rspamd" is listed twice? How do you make sure that it is NOT +;; printed twice in smtpd.conf? +;; 1st flatten "total filters", then remove its duplicates. Then print all of those filters. +;; 2nd now we go through "total filters", and we only print the non-filter-chains. +(define (opensmtpd-filters->list-of-strings-and-gexps filters) + ;; first display the unique s. and s. + ;; to do this: flatten filters, then remove duplicates. + (list + (apply string-append + (map (lambda (filter) + (if (opensmtpd-filter-phase? filter) + (opensmtpd-filter-phase->string filter) + "")) + (delete-duplicates (flatten filters)))) + ;; print out the filter-configurations + ;; would values and or call-with-values and or recieve work here? + (list (map (lambda (filter) + (if (opensmtpd-filter? filter) + (list "filter " + "\"" (opensmtpd-filter-name filter) "\" " + (if (opensmtpd-filter-exec filter) + "proc-exec " + "proc ") + "\"" (opensmtpd-filter-proc filter) "\"" + "\n\n") + "")) + (delete-duplicates (flatten filters)))) + ;; now we have to print the filter chains. + (apply string-append + (map (lambda (filter) + (cond ((list? filter) + (opensmtpd-filter-chain->string filter)) + (else ; you are a + ""))) + filters)))) + +(define (opensmtpd-configuration-listen->string string) + (string-append + "include \"" string "\"\n")) + +(define (opensmtpd-configuration-srs->string record) + (let ((key (opensmtpd-srs-key record)) + (backup-key (opensmtpd-srs-backup-key record)) + (ttl-delay (opensmtpd-srs-ttl-delay record))) + (string-append + (variable->string key #:append "srs key " #:postpend "\n") + (variable->string backup-key #:append "srs key backup " #:postpend "\n") + (variable->string ttl-delay #:append "srs ttl " #:postpend "\n") + "\n"))) + +;; TODO make sure all options here work! I just fixed limit-max-rcpt! +(define (opensmtpd-smtp->string record) + (let ((ciphers (opensmtpd-smtp-ciphers record)) + (limit-max-mails (opensmtpd-smtp-limit-max-mails record)) + (limit-max-rcpt (opensmtpd-smtp-limit-max-rcpt record)) + (max-message-size (opensmtpd-smtp-max-message-size record)) + (sub-addr-delim (opensmtpd-smtp-sub-addr-delim record))) + (string-append + (variable->string ciphers #:append "smtp ciphers " #:postpend "\n") + (variable->string limit-max-mails #:append "smtp limit max-mails " #:postpend "\n") + (variable->string limit-max-rcpt #:append "smtp limit max-rcpt " #:postpend "\n") + (variable->string max-message-size #:append "smtp max-message-size " #:postpend "\n") + (variable->string sub-addr-delim #:append "smtp sub-addr-delim " #:postpend "\n") + "\n"))) + +(define (opensmtpd-configuration-queue->string record) + (let ((compression (opensmtpd-queue-compression record)) + (encryption (opensmtpd-queue-encryption record)) + (ttl-delay (opensmtpd-queue-ttl-delay record))) + (string-append + (if compression + "queue compression\n" + "") + (if encryption + (string-append + "queue encryption " + (if (not (boolean? encryption)) + encryption + "") + "\n") + "") + (if ttl-delay + (string-append "queue ttl" ttl-delay "\n") + "")))) + +;; build a list of from +;; opensmtpd-configuration-matches, which is a list of . +;; Each has a fieldname 'action', which accepts an . +(define (get-opensmtpd-actions record) + (define opensmtpd-actions + (let loop ((list (opensmtpd-configuration-matches record))) + (if (null? list) + '() + (cons (opensmtpd-match-action (car list)) + (loop (cdr list)))))) + (delete-duplicates (append opensmtpd-actions))) + +;; build a list of opensmtpd-pkis from +;; opensmtpd-configuration-listen-ons and +;; get-opensmtpd-actions +(define (get-opensmtpd-pkis record) + ;; TODO/FIXME/maybe/wishlist could get-opensmtpd-actions -> NOT have an opensmtpd-relay? + ;; I think so. And if it did NOT have a relay configuration, then action-pkis would be '() when + ;; it needs to be #f. because if the opensmtpd-configuration has NO pkis, then this function will + ;; return '(), when it should return #f. If it returns '(), then opensmtpd-configuration-fieldname->string will + ;; print the string "\n" instead of "" + (define action-pkis + (let loop1 ((list (get-opensmtpd-actions record))) + (if (null? list) + '() + (if (and (opensmtpd-relay? (car list)) + (opensmtpd-relay-pki (car list))) + (cons (opensmtpd-relay-pki (car list)) + (loop1 (cdr list))) + (loop1 (cdr list)))))) + ;; FIXME/TODO/maybe/wishlist + ;; this could be #f aka left blank. aka there are no listen-ons records with pkis. + ;; aka there are no lines in the configuration like: + ;; listen on eth0 tls pki smtp.gnucode.me in that case the smtpd.conf will have an extra "\n" + (define listen-on-pkis + (let loop2 ((list (opensmtpd-configuration-listen-ons record))) + (if (null? list) + '() + (if (opensmtpd-interface-pki (car list)) + (cons (opensmtpd-interface-pki (car list)) + (loop2 (cdr list))) + (loop2 (cdr list)))))) + (delete-duplicates (append action-pkis listen-on-pkis))) + +;; takes in a and returns a list whose elements are , +;; , and a filter-chain. +;; It returns a list of and/or +;; here's an example of what this procedure might return: +;; (list (opensmtpd-filter...) (opensmtpd-filter-phase ...) +;; (openmstpd-filter ...) (opensmtpd-filter-phase ...) +;; ;; this next list is a filter-chain. +;; (list (opensmtpd-filter-phase ...) (opensmtpd-filter...))) +;; +;; This procedure handles filter chains a little odd. +(define (get-opensmtpd-filters record) + (define list-of-listen-on-records (if (opensmtpd-configuration-listen-ons record) + (opensmtpd-configuration-listen-ons record) + '())) + + (define listen-on-socket-filters + (if (opensmtpd-socket-configuration-filters (opensmtpd-configuration-listen-on-socket record)) + (opensmtpd-socket-configuration-filters (opensmtpd-configuration-listen-on-socket record)) + '())) + + (delete-duplicates + (append (remove boolean? + (map-in-order (lambda (listen-on-record) ; get the filters found in the s + (if (and (opensmtpd-interface-filters listen-on-record) + (= 1 (length (opensmtpd-interface-filters + listen-on-record)))) + (car (opensmtpd-interface-filters listen-on-record)) + (opensmtpd-interface-filters listen-on-record))) + list-of-listen-on-records)) + listen-on-socket-filters))) + +(define (flatten . lst) + "Return a list that recursively concatenates all sub-lists of LST." + (define (flatten1 head out) + (if (list? head) + (fold-right flatten1 out head) + (cons head out))) + (fold-right flatten1 '() lst)) + +;; This function takes in a record, or list, or anything, and returns +;; a list of s assuming the thing you passed into it had +;; any s. +;; +;; is object record? call func on it's fieldnames +;; is object list? loop through it's fieldnames calling func on it's records +;; is object #f or string? or '()? -> #f +(define (get-opensmtpd-tables value) + (delete-duplicates + (remove boolean? (flatten ;; turn (list '(1) '(2 '(3))) -> '(1 2 3) + (cond ((opensmtpd-table? value) + value) + ((record? value) + (let* ((record-type (record-type-descriptor value)) + (list-of-record-fieldnames (record-type-fields record-type))) + (map (lambda (fieldname) + (get-opensmtpd-tables ((record-accessor record-type fieldname) value))) + list-of-record-fieldnames))) + ((and (list? value) (not (null? value))) + (map get-opensmtpd-tables value)) + (else #f)))))) + +(define (opensmtpd-configuration-fieldname->string record fieldname-accessor record->string) + (if (fieldname-accessor record) + (begin + (string-append + (list-of-records->string (fieldname-accessor record) record->string) "\n")) + "")) + +(define (list-of-records->string list-of-records record->string) + (string-append + (cond ((not (list? list-of-records)) + (record->string list-of-records)) + (else + (let loop ([list list-of-records]) + (if (null? list) + "" + (string-append + (record->string (car list)) + (loop (cdr list))))))))) + +(define (opensmtpd-configuration->string record) + (string-append + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-bounce + (lambda (%bounce) + (if %bounce + (list-of-strings->string %bounce) + ""))) + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-smtp + opensmtpd-smtp->string) + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-srs + opensmtpd-configuration-srs->string) + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-queue + opensmtpd-configuration-queue->string) + ;; write out the mta-max-deferred + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-mta-max-deferred + (lambda (var) + (string-append "mta max-deferred " + (number->string (opensmtpd-configuration-mta-max-deferred record)) "\n"))) + ;;write out all the tables + (opensmtpd-configuration-fieldname->string record get-opensmtpd-tables opensmtpd-table->string) + ;; write out all the cas + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-cas opensmtpd-ca->string) + ;; write out all the pkis + (opensmtpd-configuration-fieldname->string record get-opensmtpd-pkis opensmtpd-pki->string) + ;; write all of the listen-on-records + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-listen-ons + opensmtpd-interface->string) + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-listen-on-socket + opensmtpd-socket->string) + ;; write all the actions + (opensmtpd-configuration-fieldname->string record get-opensmtpd-actions + opensmtpd-action->string) + ;; write all of the matches + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-matches opensmtpd-match->string))) + +;; FIXME/TODO should I use format here srfi-28 ? +;; web.scm nginx does a (format #f "string" "another string") +;; this could be a list like (list (file-append opensmtpd-dkimsign "/libexec/filter") "-d gnucode.me -s /path/to/selector.cert") +;; Then opensmtpd-configuration->mixed-text-file could be rewritten to be something like +;; (mixed-text-file (eval `(string-append (opensmtpd-configuration-fieldname->string ...)) (gnu services mail))) +(define (opensmtpd-configuration->mixed-text-file record) + ;; should I use this named let, or should I give this a name, or not use it at all... + ;; eg: (write-all-fieldnames (list (cons fieldname fieldname->string) (cons fieldname2 fieldname->string))) + ;; (let loop ([list (list (cons opensmtpd-configuration-includes (lambda (string) + ;; (string-append + ;; "include \"" string "\"\n"))) + ;; (cons opensmtpd-configuration-smtp opensmtpd-smtp->string) + ;; (cons opensmtpd-configuration-srs opensmtpd-srs->string))]) + ;; (if (null? list) + ;; "" + ;; (string-append (opensmtpd-configuration-fieldname->string record + ;; (caar list) + ;; (cdar list)) + ;; (loop (cdr list))))) + + (apply mixed-text-file "smtpd.conf" + ;; write out the includes + (flatten (list + (opensmtpd-configuration-fieldname->string record opensmtpd-configuration-includes + opensmtpd-configuration-listen->string) + ;; TODO should I change the below line of code into these two lines of code? + ;;(opensmtpd-configuration-fieldname->string record get-opensmtpd-filters-and-filter-phases opensmtpd-filter-and-filter-phase->string) + ;;(opensmtpd-configuration-fieldname->string record get-opensmtpd-filter-chains opensmtpd-filter-chain->string) + ;; write out all the filters + (opensmtpd-filters->list-of-strings-and-gexps (get-opensmtpd-filters record)) + (opensmtpd-configuration->string record))))) + + (define %default-opensmtpd-config-file (plain-file "smtpd.conf" " listen on lo @@ -1668,7 +3724,7 @@ (define %default-opensmtpd-config-file match from local for any action outbound ")) -(define opensmtpd-shepherd-service +(define (opensmtpd-shepherd-service config) (match-lambda (($ package config-file) (list (shepherd-service @@ -1677,7 +3733,8 @@ (define opensmtpd-shepherd-service (documentation "Run the OpenSMTPD daemon.") (start (let ((smtpd (file-append package "/sbin/smtpd"))) #~(make-forkexec-constructor - (list #$smtpd "-f" #$config-file) + (list #$smtpd "-f" (or #$config-file + #$(opensmtpd-configuration->mixed-text-file config))) #:pid-file "/var/run/smtpd.pid"))) (stop #~(make-kill-destructor))))))) @@ -1700,10 +3757,11 @@ (define %opensmtpd-accounts (home-directory "/var/empty") (shell (file-append shadow "/sbin/nologin"))))) -(define opensmtpd-activation +(define (opensmtpd-activation config) (match-lambda (($ package config-file) - (let ((smtpd (file-append package "/sbin/smtpd"))) + (let ((smtpd (file-append package "/sbin/smtpd")) + (configuration (opensmtpd-configuration->mixed-text-file config))) #~(begin (use-modules (guix build utils)) ;; Create mbox and spool directories. @@ -1711,7 +3769,12 @@ (define opensmtpd-activation (mkdir-p "/var/spool/smtpd") (chmod "/var/spool/smtpd" #o711) (mkdir-p "/var/spool/mail") - (chmod "/var/spool/mail" #o711)))))) + (chmod "/var/spool/mail" #o711) + (display (string-append "checking syntax of " + (or + #$config-file + #$configuration) + "\n"))))))) (define %opensmtpd-pam-services (list (unix-pam-service "smtpd"))) diff --git a/gnu/tests/mail.scm b/gnu/tests/mail.scm index f13751b72f..1bac9f50a2 100644 --- a/gnu/tests/mail.scm +++ b/gnu/tests/mail.scm @@ -37,6 +37,7 @@ (define-module (gnu tests mail) #:use-module (guix gexp) #:use-module (guix store) #:use-module (ice-9 ftw) + #:use-module (srfi srfi-64) #:export (%test-opensmtpd %test-exim %test-dovecot @@ -165,6 +166,360 @@ (define %test-opensmtpd (description "Send an email to a running OpenSMTPD server.") (value (run-opensmtpd-test)))) +;; trying to create a bad record, should result in an error. +;; this function should be able return, instead it should throw an error +(define (create-bad-record record) + ;; TODO why is this not working + (with-output-to-port (%make-void-port "w") + (lambda () (when record #f)))) + +;; if this caller function is reached, then trying to create the bad record +;; resulted in an error. So return true. +(define (return-true error arg) + #t) + +;; two filters with the same name +(define (bad-interface1) + (create-bad-record + (opensmtpd-interface + (interface "lo") + (filters (list + (opensmtpd-filter + (name "dkimsign") + (exec #t) + (proc (list (file-append opensmtpd-filter-dkimsign "/libexec/opensmtpd/filter-dkimsign") + " -d gnucode.me -s 2021-09-22 -c relaxed/relaxed -k " + "rando string" + "/etc/dkim/private.key " + "user nobody group nogroup"))) + (opensmtpd-filter + (name "dkimsign") + (exec #t) + (proc (list (file-append opensmtpd-filter-dkimsign "/libexec/opensmtpd/filter-dkimsign") + " -d gnucode.me -s 2021-09-22 -c relaxed/relaxed -k " + "/etc/dkim/private.key " + "user nobody group nogroup")))))))) + +;; duplicate filter names +(define (bad-interface2) + (create-bad-record + (opensmtpd-interface + (filters (list + (opensmtpd-filter-phase + (name "src") + (phase "connect") + (options + (list + (opensmtpd-option + (option "fcrdns") + (not #t)))) + (decision "junk")) + (opensmtpd-filter-phase + (name "src") + (phase "helo") + (options + (list + (opensmtpd-option + (option "rdns") + (not #t)))) + (decision "junk"))))))) + + ;; improper phase name +(define (bad-filter-phase1) + (create-bad-record + (opensmtpd-filter-phase + (name "filter") + (phase "wrongString") + (decision "bypass") + (options + (list + (opensmtpd-option + (option "auth"))))))) + +;; decision reject requires you to have a +;; corresponding fieldname 'message' with value of string. +(define (bad-filter-phase2) + (create-bad-record + (opensmtpd-interface + (filters (list + (opensmtpd-filter-phase + (name "src") + (phase "connect") + (options + (list + (opensmtpd-option + (option "src") + (data (opensmtpd-table + (name "src-table") + (data (list "cat" "hat"))))))) + (decision "reject"))))))) + +;; message needs to start with 4xx or 5xx +(define (bad-filter-phase3) + (create-bad-record + (opensmtpd-interface + (filters (list + (opensmtpd-filter-phase + (name "src") + (phase "connect") + (options + (list + (opensmtpd-option + (option "src") + (data (opensmtpd-table + (name "src-table") + (data (list "cat" "hat"))))))) + (decision "reject") + (message "322 Bad data!"))))))) + + ;; there needs to be a value here. rewrite requires a value! +(define (bad-filter-phase4) + (create-bad-record + (opensmtpd-interface + (filters + (list + (opensmtpd-filter-phase + (name "noFRDNS") + (phase "commit") + (options (list (opensmtpd-option + (option "fcrdns") + (not #t)))) + (decision "rewrite")) + ))))) + +;; fieldname 'decision' with value "junk" or "bypass", then fieldname 'message' and 'value' +;; must NOT be defined +(define (bad-filter-phase5) + (create-bad-record + (opensmtpd-interface + (filters + (list + (opensmtpd-filter-phase + (name "noFRDNS") + (phase "commit") + (options (list (opensmtpd-option + (option "fcrdns") + ))) + (decision "junk") + (message "This is not a good email."))))))) + +;; you cannot junk on phase commit. You need to use an eariler phase. +(define (bad-filter-phase6) + (create-bad-record + (opensmtpd-interface + (filters + (list + (opensmtpd-filter-phase + (name "junk-after-commit") + (options (list (opensmtpd-option + (option "fcrdns")))) + (phase "commit") + (decision "junk"))))))) + +;; TODO fix this test +;; two fcrdns options records +(define (bad-filter-phase7) + (create-bad-record + (opensmtpd-filter-phase + (name "invalid-fcrdns") + (phase "connect") + (options + (list (opensmtpd-option + (option "fcrdns") + (not #t)) + (opensmtpd-option + (option "fcrdns") + (not #f)))) + (decision "reject") + (message "422 No valid fcrdns.")))) + +;; option src requires a table +;; TODO maybe check for other options requiring a table +(define (bad-filter-phase8) + (create-bad-record + (opensmtpd-filter-phase + (name "filter") + (phase "helo") + (decision "bypass") + (options + (list + (opensmtpd-option + (option "src"))))))) + +;; option fcrdns cannot have data defined. +(define (bad-filter-phase9) + (create-bad-record + (opensmtpd-filter-phase + (name "filter") + (phase "helo") + (decision "bypass") + (options + (list + (opensmtpd-option + (option "fcrdns") + (data (opensmtpd-table + (name "table") + (data (list "hello" "cat")))))))))) + + +;; this should be (list ...) instead of '( ...) +(define (bad-match1) + (create-bad-record + (opensmtpd-match + (options + '((opensmtpd-option + (option "for any")))) + (action + (opensmtpd-relay))))) + + +;; duplcate "for" options +(define (bad-match2) + (create-bad-record + (opensmtpd-match + (options (list + (opensmtpd-option + (option "for any")) + (opensmtpd-option + (option "for local")))) + (action + (opensmtpd-relay + (name "relay")))))) + +;; duplicate froms +(define (bad-match3) + (create-bad-record + (opensmtpd-match + (options (list + (opensmtpd-option + (option "from any")) + (opensmtpd-option + (option "from auth")))) + (action + (opensmtpd-relay + (name "relay")))))) + +;; rcpt-to must have a data field. +(define (bad-match4) + (create-bad-record + (opensmtpd-match + (options (list + (opensmtpd-option + (option "rcpt-to")))) + (action + (opensmtpd-relay + (name "relay")))))) + +;; option 'tls' cannot have fieldname +;; 'data' defined. +(define (bad-match5) + (create-bad-record + (opensmtpd-match + (options (list + (opensmtpd-option + (option "tls") + (data "hello")))) + (action + (opensmtpd-relay + (name "relay")))))) + +;; for any cannot have data +;; or regex defined +(define (bad-match6) + (create-bad-record + (opensmtpd-match + (options (list + (opensmtpd-option + (option "for any") + (regex #t)))) + (action + (opensmtpd-relay + (name "relay")))))) + +;; match needs an action +(define (bad-match7) + (create-bad-record + (opensmtpd-match + (options (list + (opensmtpd-option + (option "from auth"))))))) + +(define (run-opensmtpd-record-sanitation-test) + ;(with-output-to-port (%make-void-port "w") + ; (lambda () + (test-begin "run-opensmtpd-record-sanitation-test") + + ;; TODO fix me! + (test-assert "Test fieldname 'filters' has two filters with the same name." + (catch #t bad-interface1 return-true)) + + (test-assert "Test cannot have two filters with the same name." + (catch #t bad-interface2 return-true)) + + (test-assert "Test fieldname 'phase' the right string." + (catch #t bad-filter-phase1 return-true)) + + (test-assert "Test fieldname 'decision' w/ value \"reject\" and \"disconnect\" requires a 'message'." + (catch #t bad-filter-phase2 return-true)) + + (test-assert (string-append "Test fieldname 'decision' " + "w/ value \"reject\" and \"disconnect\" requires a 'message'." + " The message must begin with 4xx or 5xx.") + (catch #t bad-filter-phase3 return-true)) + + (test-assert "Test fieldname 'rewrite' requires fieldname 'value' to have a number." + (catch #t bad-filter-phase4 return-true)) + + (test-assert (string-append "Test fieldname 'decision' with values 'junk' or 'bypass', " + "then fieldname 'message' and 'value' must be blank.") + (catch #t bad-filter-phase5 return-true)) + + (test-assert "You cannot junk an email on phase commit." + (catch #t bad-filter-phase6 return-true)) + + ;; TODO fix me! + (test-assert "Test has 2 duplicate options." + (catch #t bad-filter-phase7 return-true)) + + (test-assert "Test option 'src' requires a table." + (catch #t bad-filter-phase8 return-true)) + + ;; TODO fix me! + (test-assert "Test option 'fcrdns' cannot have a table." + (catch #t bad-filter-phase9 return-true)) + + (test-assert "Test fieldname 'options' should not be quoted." + (catch #t bad-match1 return-true)) + + (test-assert "Test has duplicate 'for' options." + (catch #t bad-match2 return-true)) + + (test-assert "Test has duplicate 'from' options." + (catch #t bad-match3 return-true)) + + (test-assert "Test option 'rcpt' must have data." + (catch #t bad-match4 return-true)) + + (test-assert "Test option 'tls' cannot have fieldname 'data' defined." + (catch #t bad-match5 return-true)) + + (test-assert "Test option 'for any' cannot have fieldname 'data' defined." + (catch #t bad-match6 return-true)) + + (test-assert "Test needs fieldname 'action' needs to be defined." + (catch #t bad-match7 return-true)) + + (test-end "run-opensmtpd-record-sanitation-test")) + +(define %test-opensmtpd-record-sanitation + (system-test + (name "opensmtpdRecordSanitation") + (description + (string-append " has numerous sanity checks.\n" + "This checks that invalid configurations, return an\n" + "appropriate error.\n")) + (value (run-opensmtpd-record-sanitation-test)))) + (define %exim-os (simple-operating-system base-commit: 4b3493ed0156709a924f31ef4c9a5efa0815dfe8 -- 2.38.0 From debbugs-submit-bounces@debbugs.gnu.org Mon Oct 24 14:29:09 2022 Received: (at 56046) by debbugs.gnu.org; 24 Oct 2022 18:29:10 +0000 Received: from localhost ([127.0.0.1]:49538 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1on2CD-0006RR-ST for submit@debbugs.gnu.org; Mon, 24 Oct 2022 14:29:09 -0400 Received: from mail-ej1-f67.google.com ([209.85.218.67]:39663) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1on2CC-0006RE-IY for 56046@debbugs.gnu.org; Mon, 24 Oct 2022 14:29:09 -0400 Received: by mail-ej1-f67.google.com with SMTP id b2so7030570eja.6 for <56046@debbugs.gnu.org>; Mon, 24 Oct 2022 11:29:08 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=mime-version:user-agent:content-transfer-encoding:references :in-reply-to:date:to:from:subject:message-id:from:to:cc:subject:date :message-id:reply-to; bh=y3X0QSYHrSU1rcjQiPQF2TlLPFCjUKd/13zvDh+OKr8=; b=QyMUvgMGmmaEKwLg58F5KkXSLpaibzPMCHY4N+QH4ekrl9cRUrtPEeRxYyP2PCFcx5 7j72AslI8Ac/p2fuZbxNGKAzqggpIxDjTAhYk4NAWebj2etncFsbndNKq0WeFKkKbM8f SZfn6BKUtce5dPZI7//rg8vHua+v8dvRjTeaHht/kUzWb6YAfd61UKOca3oNOLDwQorr AxA28KlNHZGIUbfywaq3ycVpIViO+hkss/K9uHqNCzmun14uLUKhF5EttcfOL1KOI1B6 O3759lI3w5HmGujhTB53uaBKKuvCdpYzms7DVvfu0wNA29HJ/ArqskCx3M6VftwfLElw 57LQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=mime-version:user-agent:content-transfer-encoding:references :in-reply-to:date:to:from:subject:message-id:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=y3X0QSYHrSU1rcjQiPQF2TlLPFCjUKd/13zvDh+OKr8=; b=E+m1G8TIqL7yAa6CQPrNDeLBeFbADqPvEOiSG9jOTiNYa8vZsX5X3gX/UVEqRG0QXs GjYOde/BeXXucgCfLwaaX8wQG/NGv6JjloCUuJVrTXhj8qN0kqBLpMrSfzmra2C6OkuV NeTuHBbvrCpkNeLDSQTRFKvWJuUcYfeff1bFHA5K2tcGs0tj/q1It+hBPA0+cWgHI2I+ Y0B0GbBnQrC79UM2e+KCJb+jLbeonQnliQepbsGx5E9lgNDNr2wU7y2zdO843IcvWeHp GUSYrViz2w68+PKZegA+3o0YXLm9HhJZT/ASmjW9EeMviT6UW1FE+NaJppqo92RGkTJj 03qw== X-Gm-Message-State: ACrzQf1G1saAlD1+Yu0UBlix0l3HbI2P0VZj433HE6d0C/D6sAOUqYOg dhtlNCVS1caD589+CBr6pXEMXIdxlj0= X-Google-Smtp-Source: AMsMyM59XA8QdkBbaoG+AJoxtccRUXsoW98fclIBxj0BtirJziV86XCklqhyg1gHTmee4IhMy6H2fA== X-Received: by 2002:a17:906:fe45:b0:788:15a5:7495 with SMTP id wz5-20020a170906fe4500b0078815a57495mr29246786ejb.633.1666636140641; Mon, 24 Oct 2022 11:29:00 -0700 (PDT) Received: from lumine.fritz.box (85-127-52-93.dsl.dynamic.surfer.at. [85.127.52.93]) by smtp.gmail.com with ESMTPSA id b12-20020a1709063f8c00b0078d4e39d87esm198028ejj.225.2022.10.24.11.28.59 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 24 Oct 2022 11:28:59 -0700 (PDT) Message-ID: Subject: Re: [Patch master v2] services (opensmtpd): add opensmtpd records to enhance opensmtpd-configuration. From: Liliana Marie Prikler To: Joshua Branson , 56046@debbugs.gnu.org Date: Mon, 24 Oct 2022 20:28:57 +0200 In-Reply-To: References: Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: base64 User-Agent: Evolution 3.46.0 MIME-Version: 1.0 X-Debbugs-Envelope-To: 56046 X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" QW0gTW9udGFnLCBkZW0gMjQuMTAuMjAyMiB1bSAxMzozMCAtMDQwMCBzY2hyaWViIEpvc2h1YSBC cmFuc29uOgo+IFRoaXMgaXMgYSBWMiBwYXRjaC7CoCBJJ3ZlIGFkZGVkIHNvbWUgdGVzdHMgdGhh dCBoZWxwIHRlc3QgZm9yIHZhcmlvdXMKPiB3YXlzIHRoYXQgdXNlcnMgY291bGQgYWNjaWRlbnRh bGx5IG1pc2NvbmZpZ3VyZSB0aGVpciBjb25maWd1cmF0aW9uLgo+IAo+IEkgcHJvYmFibHkgbmVl ZCB0byBtYWtlIHRob3NlIGVycm9yIG1lc3NhZ2VzLCB1c2XCoMKgIAo+IChndWl4IGRpYWdub3N0 aWNzKS7CoCBDdXJyZW50bHkgY29tcGlsaW5nIHRoZSB0ZXN0cywgYXV0byBydW5zIHRoZW0uCj4g U28gIm1ha2UiIGF1dG8gcnVucyB0aGUgdGVzdHMuwqAgQWxzbyB0aGVpciBlcnJvciBtZXNzYWdl cyBhcmUgb3V0cHV0Cj4gdG8gdGhlIHRlcm1pbmFsLCBhbmQgSSdtIG5vdCBzdXJlIGhvdyB0byB0 dXJuIHRoYXQgb2ZmLgo+IAo+IHRsO2RyIHRoaXMgaXMgYSBXSVAgcGF0Y2gsIGFuZCBJIGp1c3Qg d2FudGVkIHRvIHN1Ym1pdCBzb21ldGhpbmcsCj4gYmVjYXVzZSBJIGtlZXAgZmluZGluZyBtb3Jl IHRoaW5ncyB0aGF0IEkgbmVlZCB0byBmaXguCj4gCj4gVGhlIHRhc2sgbGlzdCBhcyBhbHdheXMg aXMgaGVyZToKPiBodHRwczovL25vdGFidWcub3JnL2picmFuc28vbGlub2RlLWd1aXgtc3lzdGVt LWNvbmZpZ3VyYXRpb24vc3JjL21hc3Rlci9vcGVuc210cGQub3JnCj4gwqAKPiAKPiBPcGVubXN0 cGQtY29uZmlndXJhdGlvbiBtYXkgb25seSBiZSBjb25maWd1cmVkIGJ5IGEgY29uZmlnLWZpbGUg dGhhdAo+IHVzZXMgdGhlIHNtdHBkLmNvbmYgc3ludGF4LsKgIFRoaXMgcGF0Y2gsIGVuYWJsZXMg b25lIHRvIGNvbmZpZ3VyZQo+IG9wZW5zbXRwZCBieSB1c2luZyByZWNvcmQgdHlwZXMuCj4gCj4g KiBnbnUvc2VydmljZXMvbWFpbC5zY206Cj4gKG9wZW5zbXRwZC10YWJsZS1jb25maWd1cmF0aW9u LApDaGFuZ2VMb2cgZm9ybWF0IHdvdWxkIGJlIChvcGVuc210cGQtdGFibGUtY29uZmlndXJhdGlv bikgZm9sbG93ZWQgYnkgYQpuZXcgbGluZSwgZm9sbG93ZWQgYnkgKG9wZW5zbXRwZC1jYS1jb25m aWd1cmF0aW9uKSBldGMuCj4gb3BlbnNtdHBkLWNhLWNvbmZpZ3VyYXRpb24sCj4gb3BlbnNtdHBk LXBraS1jb25maWd1cmF0aW9uLAo+IG9wZW5zbXRwZC1hY3Rpb24tbG9jYWwtZGVsaXZlcnktY29u ZmlndXJhdGlvbiwKPiBvcGVuc210cGQtbWFpbGRpci1jb25maWd1cmF0aW9uLAo+IG9wZW5zbXRw ZC1tZGEtY29uZmlndXJhdGlvbiwKPiBvcGVuc210cGQtYWN0aW9uLXJlbGF5LWNvbmZpZ3VyYXRp b24sCj4gb3BlbnNtdHBkLW9wdGlvbi1jb25maWd1cmF0aW9uLAo+IG9wZW5zbXRwZC1maWx0ZXIt cGhhc2UtY29uZmlndXJhdGlvbiwKPiBvcGVuc210cGQtZmlsdGVyLWNvbmZpZ3VyYXRpb24sCj4g b3BlbnNtdHBkLWludGVyZmFjZSwKPiBvcGVuc210cGQtc29ja2V0LAo+IG9wZW5zbXRwZC1tYXRj aC1jb25maWd1cmF0aW9uLAo+IG9wZW5zbXRwZC1zbXRwLWNvbmZpZ3VyYXRpb24sCj4gb3BlbnNt dHBkLXNycy1jb25maWd1cmF0aW9uLAo+IG9wZW5zbXRwZC1xdWV1ZS1jb25maWd1cmF0aW9uLCBh bmQKPiBvcGVuc210cGQtY29uZmlndXJhdGlvbik6IE5ldyByZWNvcmRzLgo+IAo+IChmYWxzZT8s IGlzLXZhbHVlLXJpZ2h0LXR5cGUsIGFkZC1jb21tYS1vci1zdHJpbmcsCj4gbGlzdC1vZi1wcm9j ZWR1cmVzLT5zdHJpbmcsIHN0cmluZy1pbi1saXN0PywgbXktc2FuaXRpemUsCj4gb3BlbnNtdHBk LWZpbHRlci1jaGFpbj8sIHRocm93LWVycm9yLWR1cGxpY2F0ZS1vcHRpb24sCj4gc2FuaXRpemUt bGlzdC1vZi1vcHRpb25zLWZvci1tYXRjaC1jb25maWd1cmF0aW9uLCBzYW5pdGl6ZS1maWx0ZXJz LAo+IGxpc3QtaGFzLWR1cGxpY2F0ZXMtb3Itbm9uLWZpbHRlcnM/LAo+IGZpbHRlci1waGFzZS1o YXMtbWVzc2FnZS1hbmQtdmFsdWU/LAo+IGZpbHRlci1waGFzZS1kZWNpc2lvbi1sYWNrcy1wcm9w ZXItbWVzc2FnZT8sCj4gZmlsdGVyLXBoYXNlLWxhY2tzLXByb3Blci12YWx1ZT8sCj4gZmlsdGVy LXBoYXNlLWhhcy1pbmNvcnJlY3QtanVuay1vci1ieXBhc3M/LAo+IGZpbHRlci1waGFzZS1qdW5r cy1hZnRlci1jb21taXQ/LAo+IGxpc3Qtb2YtdW5pcXVlLWZpbHRlci1vci1maWx0ZXItcGhhc2U/ LCB0aHJvdy1lcnJvciwKPiBjb250YWlucy1kdXBsaWNhdGU/LCBsaXN0LW9mLXR5cGU/LCBsaXN0 LW9mLXN0cmluZ3M/LAo+IGxpc3Qtb2YtdW5pcXVlLW9wZW5zbXRwZC1vcHRpb24tY29uZmlndXJh dGlvbj8sCj4gbGlzdC1vZi1vcGVuc210cGQtY2EtY29uZmlndXJhdGlvbj8sCj4gbGlzdC1vZi1v cGVuc210cGQtcGtpLWNvbmZpZ3VyYXRpb24/LAo+IGxpc3Qtb2Ytb3BlbnNtdHBkLWxpc3Rlbi1v bi1jb25maWd1cmF0aW9uPywKPiBsaXN0LW9mLXVuaXF1ZS1vcGVuc210cGQtbWF0Y2gtY29uZmln dXJhdGlvbj8sIGxpc3Qtb2Ytc3RyaW5ncy0KPiA+c3RyaW5nLAo+IGFzc29jLWxpc3Q/IGFzc29j LWxpc3QsIHZhcmlhYmxlLT5zdHJpbmcsCj4gdGFibGUtd2hvc2UtZGF0YS1hcmUtYXNzb2MtbGlz dD8sCj4gdGFibGUtd2hvc2UtZGF0YS1hcmUtYS1saXN0LW9mLXN0cmluZ3M/LCBhc3NvYy1saXN0 LT5zdHJpbmcsCj4gb3BlbnNtdHBkLXRhYmxlLWNvbmZpZ3VyYXRpb24tPnN0cmluZywKPiBvcGVu c210cGQtbGlzdGVuLW9uLWNvbmZpZ3VyYXRpb24tPnN0cmluZywKPiBvcGVuc210cGQtbGlzdGVu LW9uLXNvY2tldC1jb25maWd1cmF0aW9uLT5zdHJpbmcsCj4gb3BlbnNtdHBkLWFjdGlvbi1yZWxh eS1jb25maWd1cmF0aW9uLT5zdHJpbmcsCj4gb3BlbnNtdHBkLWxtdHAtY29uZmlndXJhdGlvbi0+ c3RyaW5nLAo+IG9wZW5zbXRwZC1tZGEtY29uZmlndXJhdGlvbi0+c3RyaW5nLAo+IG9wZW5zbXRw ZC1tYWlsZGlyLWNvbmZpZ3VyYXRpb24tPnN0cmluZywKPiBvcGVuc210cGQtYWN0aW9uLWxvY2Fs LWRlbGl2ZXJ5LWNvbmZpZ3VyYXRpb24tPnN0cmluZywKPiBvcGVuc210cGQtYWN0aW9uLT5zdHJp bmcsIG9wZW5zbXRwZC1vcHRpb24tY29uZmlndXJhdGlvbi0+c3RyaW5nLAo+IG9wZW5zbXRwZC1t YXRjaC1jb25maWd1cmF0aW9uLT5zdHJpbmcsCj4gb3BlbnNtdHBkLWNhLWNvbmZpZ3VyYXRpb24t PnN0cmluZywgb3BlbnNtdHBkLXBraS1jb25maWd1cmF0aW9uLQo+ID5zdHJpbmcsCj4gZ2VuZXJh dGUtZmlsdGVyLWNoYWluLW5hbWUsIG9wZW5zbXRwZC1maWx0ZXItY2hhaW4tPnN0cmluZywKPiBv cGVuc210cGQtZmlsdGVyLXBoYXNlLWNvbmZpZ3VyYXRpb24tPnN0cmluZywgb3BlbnNtdHBkLWZp bHRlcnMtCj4gPnN0cmluZywKPiBvcGVuc210cGQtY29uZmlndXJhdGlvbi1saXN0ZW4tPnN0cmlu ZywKPiBvcGVuc210cGQtY29uZmlndXJhdGlvbi1zcnMtPnN0cmluZywKPiBvcGVuc210cGQtc210 cC1jb25maWd1cmF0aW9uLT5zdHJpbmcsCj4gb3BlbnNtdHBkLWNvbmZpZ3VyYXRpb24tcXVldWUt PnN0cmluZywgZ2V0LW9wZW5zbXRwZC1hY3Rpb25zLAo+IGdldC1vcGVuc210cGQtcGtpLWNvbmZp Z3VyYXRpb25zLCBnZXQtb3BlbnNtdHBkLWZpbHRlcnMsIGZsYXR0ZW4sCj4gZ2V0LW9wZW5zbXRw ZC10YWJsZXMsIG9wZW5zbXRwZC1jb25maWd1cmF0aW9uLWZpZWxkbmFtZS0+c3RyaW5nLAo+IGxp c3Qtb2YtcmVjb3Jkcy0+c3RyaW5nLCBvcGVuc210cGQtY29uZmlndXJhdGlvbi0+bWl4ZWQtdGV4 dC1maWxlKToKPiBOZXcKPiBwcm9jZWR1cmVzLgo+IAo+ICogZ251L3Rlc3RzL21haWwuc2NtIDog bmV3IHRlc3RzIGZvciB2YXJpb3VzIG9wZW5zbXRwZCByZWNvcmRzLgo+IAo+ICogZG9jL2d1aXgu dGV4aSAoT3BlblNNVFBEIFNlcnZpY2UpOiBBZGRlZCBkb2N1bWVudGF0aW9uIGZvciB0aGUKPiBu ZXcgcmVjb3JkcyBmb3Igb3BlbnNtdHBkLgo+IC0tLQo+IMKgZG9jL2d1aXgudGV4acKgwqDCoMKg wqDCoMKgwqAgfCAxMDU0ICsrKysrKysrKysrKysrKysrKysrLQo+IMKgZ251L3NlcnZpY2VzL21h aWwuc2NtIHwgMjA4NQo+ICsrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKyst Cj4gwqBnbnUvdGVzdHMvbWFpbC5zY23CoMKgwqAgfMKgIDM1NSArKysrKysrCj4gwqAzIGZpbGVz IGNoYW5nZWQsIDM0NzUgaW5zZXJ0aW9ucygrKSwgMTkgZGVsZXRpb25zKC0pCj4gCj4gZGlmZiAt LWdpdCBhL2RvYy9ndWl4LnRleGkgYi9kb2MvZ3VpeC50ZXhpCj4gaW5kZXggNTM1YzhjZGZjMy4u YzgwZjNlOWQ3NiAxMDA2NDQKPiAtLS0gYS9kb2MvZ3VpeC50ZXhpCj4gKysrIGIvZG9jL2d1aXgu dGV4aQo+IEBAIC0yNTQwOSwxNCArMjU0MDksNTkgQEAgY291bGQgaW5zdGFudGlhdGUgYSBkb3Zl Y290IHNlcnZpY2UgbGlrZQo+IHRoaXM6Cj4gwqBAc3Vic3ViaGVhZGluZyBPcGVuU01UUEQgU2Vy dmljZQo+IMKgCj4gwqBAZGVmZm4ge1NjaGVtZSBWYXJpYWJsZX0gb3BlbnNtdHBkLXNlcnZpY2Ut dHlwZQo+IC1UaGlzIGlzIHRoZSB0eXBlIG9mIHRoZSBAdXJlZntodHRwczovL3d3dy5vcGVuc210 cGQub3JnLCBPcGVuU01UUER9Cj4gLXNlcnZpY2UsIHdob3NlIHZhbHVlIHNob3VsZCBiZSBhbiBA Y29kZXtvcGVuc210cGQtY29uZmlndXJhdGlvbn0KPiBvYmplY3QKPiAtYXMgaW4gdGhpcyBleGFt cGxlOgo+IC0KPiAtQGxpc3AKPiAtKHNlcnZpY2Ugb3BlbnNtdHBkLXNlcnZpY2UtdHlwZQo+IC3C oMKgwqDCoMKgwqDCoMKgIChvcGVuc210cGQtY29uZmlndXJhdGlvbgo+IC3CoMKgwqDCoMKgwqDC oMKgwqDCoCAoY29uZmlnLWZpbGUgKGxvY2FsLWZpbGUgIi4vbXktc210cGQuY29uZiIpKSkpCj4g K09wZW5TTVRQRCBpcyBhbiBlYXN5LXRvLXVzZSBtYWlsIHRyYW5zZmVyIGFnZW50IChNVEEpLiBJ dHMKPiBjb25maWd1cmF0aW9uIGZpbGUgaXMKPiArdGhyb3VnaGx5IGRvY3VtZW50ZWQgaW4gQGNv ZGV7bWFuIDUgc210cGQuY29uZn0uIE9wZW5TTVRQRAo+IEBzdHJvbmd7bGlzdGVuc30gZm9yIGlu Y29taW5nCj4gK21haWwgYW5kIEBzdHJvbmd7bWF0Y2hlc30gdGhlIG1haWwgdG8gQHN0cm9uZ3th Y3Rpb25zfS4gVGhlCj4gZm9sbG93aW5nIHJlY29yZHMgcmVwcmVzZW50IHRob3NlCj4gK3N0YWdl czoKPiArCj4gK0BtdWx0aXRhYmxlIHthYWFhYWFhYWF9Cj4ge2FhYWFhYWFhYWFhYWFhYWFhYWFh YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWF9Cj4gK0BpdGVtIEBzdHJvbmd7bGlzdGVuc30K PiArQHRhYiBAY29kZXs8b3BlbnNtdHBkLWludGVyZmFjZT59Cj4gK0BpdGVtCj4gK0B0YWIgQGNv ZGV7PG9wZW5zbXRwZC1zb2NrZXQ+fQo+ICtAaXRlbQo+ICtAdGFiCj4gK0BpdGVtIEBzdHJvbmd7 bWF0Y2hlc30KPiArQHRhYiBAY29kZXs8b3BlbnNtdHBkLW1hdGNoPn0KPiArQGl0ZW0KPiArQHRh Ygo+ICtAaXRlbSBAc3Ryb25ne2FjdGlvbnN9Cj4gK0B0YWIgQGNvZGV7PG9wZW5zbXRwZC1sb2Nh bC1kZWxpdmVyeT59Cj4gK0BpdGVtCj4gK0B0YWIgQGNvZGV7PG9wZW5zbXRwZC1yZWxheT59Cj4g K0BlbmQgbXVsdGl0YWJsZQo+ICsKPiArQWRkaXRpb25hbGx5LCBlYWNoIEBjb2RlezxvcGVuc210 cGQtaW50ZXJmYWNlPn0gYW5kCj4gK0Bjb2RlezxvcGVuc210cGQtc29ja2V0Pn0gbWF5IHVzZSBh IGxpc3Qgb2YKPiArQGNvZGV7PG9wZW5zbXRwZC1maWx0ZXI+fSwgYW5kL29yCj4gK0Bjb2Rlezxv cGVuc210cGQtZmlsdGVyLXBoYXNlPn0gcmVjb3JkcyB0byBmaWx0ZXIgZW1haWwvc3BhbS4gQWxz bwo+ICtudW1lcm91cyByZWNvcmRzJyBmaWVsZG5hbWVzIHVzZSBAY29kZXs8b3BlbnNtdHBkLXRh YmxlPn0gdG8gaG9sZAo+IGxpc3RzCj4gK29yIGtleSB2YWx1ZSBwYWlycyBvZiBkYXRhLgo+ICsK PiArQSBzaW1wbGUgZXhhbXBsZSBjb25maWd1cmF0aW9uIGlzIGJlbG93Ogo+ICsKPiArQGxpc3AK PiArKGxldCAoKHNtdHAuZ251Lm9yZyAob3BlbnNtdHBkLXBraQo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChkb21haW4gInNtdHAuZ251Lm9yZyIpCj4g K8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGNlcnQgImZp bGUuY2VydCIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgKGtleSAiZmlsZS5rZXkiKSkpKQo+ICvCoCAoc2VydmljZSBvcGVuc210cGQtc2VydmljZS10 eXBlCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgIChvcGVuc210cGQtY29uZmlndXJhdGlvbgo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsaXN0ZW4tb25zIChsaXN0Cj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3BlbnNtdHBkLWludGVyZmFjZQo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAocGtp IHNtdHAuZ251Lm9yZykpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAob3BlbnNtdHBkLWludGVyZmFjZQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAocGtpIHNtdHAuZ251Lm9yZykKPiArwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHNlY3VyZS1jb25u ZWN0aW9uICJzbXRwcyIpKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG1hdGNoZXMgKGxp c3QKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcGVuc210 cGQtbWF0Y2gKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAg KGFjdGlvbgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg IChvcGVuc210cGQtbG9jYWwtZGVsaXZlcnkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgIChuYW1lICJsb2NhbC1kZWxpdmVyeSIpKSkpCj4gK8KgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3BlbnNtdHBkLW1hdGNoCj4g K8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChhY3Rpb24KPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3BlbnNtdHBk LXJlbGF5Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCAobmFtZSAicmVsYXkiKSkpKSkpKSkpCj4gwqBAZW5kIGxpc3AKPiDCoEBlbmQgZGVmZm4KPiDC oAo+IEBAIC0yNTQzMywxNCArMjU0NzgsMTAwNyBAQCBpdCBsaXN0ZW5zIG9uIHRoZSBsb29wYmFj ayBuZXR3b3JrCj4gaW50ZXJmYWNlLCBhbmQgYWxsb3dzIGZvciBtYWlsIGZyb20KPiDCoHVzZXJz IGFuZCBkYWVtb25zIG9uIHRoZSBsb2NhbCBtYWNoaW5lLCBhcyB3ZWxsIGFzIHBlcm1pdHRpbmcg ZW1haWwKPiB0bwo+IMKgcmVtb3RlIHNlcnZlcnMuwqAgUnVuIEBjb21tYW5ke21hbiBzbXRwZC5j b25mfSBmb3IgbW9yZSBpbmZvcm1hdGlvbi4KPiDCoAo+ICs8PDw8PDw8IEhFQUQKWW91IGhhdmUg YW4gYXJ0aWZhY3QgaGVyZS4KPiArQGl0ZW0gQGNvZGV7Ym91bmNlfSAoZGVmYXVsdDogQGNvZGV7 KGxpc3QgIjRoIil9KQo+ICsKPiArQGNvZGV7Ym91bmNlfSBpcyBhIGxpc3Qgb2Ygc3RyaW5ncywg d2hpY2ggc2VuZCB3YXJuaW5nIG1lc3NhZ2VzIHRvCj4gdGhlIGVudmVsb3BlCj4gK3NlbmRlciB3 aGVuIHRlbXBvcmFyeSBkZWxpdmVyeSBmYWlsdXJlcyBjYXVzZSBhIG1lc3NhZ2UgdG8gcmVtYWlu IGluCj4gdGhlCj4gK3F1ZXVlIGZvciBsb25nZXIgdGhhbiBzdHJpbmcgZGVsYXkuIEVhY2ggc3Ry aW5nIGRlbGF5IHBhcmFtZXRlcgo+IGNvbnNpc3RzCj4gK29mIGEgc3RyaW5nIGJlZ2lubmluZyB3 aXRoIGEgcG9zaXRpdmUgZGVjaW1hbCBpbnRlZ2VyIGFuZCBhIHVuaXQKPiAncycsICdtJywgJ2gn LAo+ICtvciAnZCcuIEF0IG1vc3QgZm91ciBkZWxheSBwYXJhbWV0ZXJzIGNhbiBiZSBzcGVjaWZp ZWQuCj4gKwo+ICtAaXRlbSBAY29kZXtsaXN0ZW4tb25zfSAoZGVmYXVsdDogQGNvZGV7KGxpc3Qg KG9wZW5zbXRwZC0KPiBpbnRlcmZhY2UpKX0pCj4gKwo+ICtAY29kZXtsaXN0ZW4tb25zfSBpcyBh IGxpc3Qgb2YgQGNvZGV7PG9wZW5zbXRwZC1pbnRlcmZhY2U+fSByZWNvcmRzLgo+ICtUaGlzIGxp c3QgZGV0YWlscyB3aGF0IGludGVyZmFjZXMgYW5kIHBvcnRzIE9wZW5TTVRQRCBsaXN0ZW5zIG9u IGFzCj4gd2VsbCBhcwo+ICtvdGhlciBpbmZvcm1hdGlvbi4KPiArCj4gK0BpdGVtIEBjb2Rle2xp c3Rlbi1vbi1zb2NrZXR9IChkZWZhdWx0OiBAY29kZXsob3BlbnNtdHBkLXNvY2tldCl9KQo+ICsK PiArTGlzdGVucyBmb3IgaW5jb21pbmcgY29ubmVjdGlvbnMgb24gdGhlIFVuaXggZG9tYWluIHNv Y2tldC4KPiArCj4gK0BpdGVtIEBjb2Rle2luY2x1ZGVzfSAoZGVmYXVsdDogQGNvZGV7I2Z9KQo+ ICsKPiArQGNvZGV7aW5jbHVkZXN9IGlzIGEgbGlzdCBvZiBzdHJpbmcgZmlsZW5hbWVzLiBFYWNo IGZpbGVuYW1lJ3MKPiBjb250ZW50cyBpcwo+ICthZGRpdGlvbmFsIGNvbmZpZ3VyYXRpb24gdGhh dCBpcyBpbnNlcnRlZCBpbnRvIHRoZSB0b3Agb2YgdGhlCj4gY29uZmlndXJhdGlvbgo+ICtmaWxl Lgo+ICsKPiArQGl0ZW0gQGNvZGV7bWF0Y2hlc30gZGVmYXVsdDoKPiArCj4gK0BsaXNwCj4gK8Kg wqDCoCAobGlzdCAob3BlbnNtdHBkLW1hdGNoCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgIChhY3Rp b24gKG9wZW5zbXRwZC1sb2NhbC1kZWxpdmVyeQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoCAobmFtZSAibG9jYWwiKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoCAobWV0aG9kICJtYm94IikpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoCAo Zm9yIChvcGVuc210cGQtb3B0aW9uCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg IChvcHRpb24gImZvciBsb2NhbCIpKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoCAob3BlbnNtdHBk LW1hdGNoCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgIChhY3Rpb24gKG9wZW5zbXRwZC1yZWxheQo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobmFtZSAib3V0Ym91bmQi KSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgIChmcm9tIChvcGVuc210cGQtb3B0aW9uCj4gK8Kg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wdGlvbiAiZnJvbSBsb2NhbCIpKSkK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGZvciAob3BlbnNtdHBkLW9wdGlvbgo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3B0aW9uICJmb3IgYW55IikpKSkpCj4gK0BlbmQg bGlzcAo+ICsKPiArQGNvZGV7bWF0Y2hlc30gaXMgYSBsaXN0IG9mIEBjb2RlezxvcGVuc210cGQt bWF0Y2g+fSByZWNvcmRzLCB3aGljaAo+ICttYXRjaGVzIGluY29taW5nIG1haWwgYW5kIHNlbmRz IGl0IHRvIGEgY29ycmVzcGVuZGluZyBhY3Rpb24uIFRoZQo+IG1hdGNoCj4gK3JlY29yZHMgYXJl IGV2YWx1YXRlZCBzZXF1ZW50aWFsbHksIHdpdGggdGhlIGZpcnN0IG1hdGNoIHdpbm5pbmcuIElm Cj4gYW4KPiAraW5jb21pbmcgbWFpbCBkb2VzIG5vdCBtYXRjaCBhbnkgbWF0Y2ggcmVjb3Jkcywg dGhlbiBpdCBpcyByZWplY3RlZC4KPiArQGMgcHV0IHRoaXMgYmFja2luPyBAZW5kIGl0ZW1pemUK PiArCj4gK0BjIHB1dCB0aGlzIGJhY2sgaW4/IEBpdGVtaXplCj4gK0BpdGVtIEBjb2Rle210YS1t YXgtZGVmZXJyZWR9IChkZWZhdWx0OiBAY29kZXsxMDB9KQo+ICsKPiArV2hlbiBkZWxpdmVyeSB0 byBhIGdpdmVuIGhvc3QgaXMgc3VzcGVuZGVkIGR1ZSB0byB0ZW1wb3JhcnkKPiBmYWlsdXJlcywg Y2FjaGUKPiArYXQgbW9zdCBudW1iZXIgZW52ZWxvcGVzIGZvciB0aGF0IGhvc3Qgc3VjaCB0aGF0 IHRoZXkgY2FuIGJlCj4gZGVsaXZlcmVkIGFzCj4gK3Nvb24gYXMgYW5vdGhlciBkZWxpdmVyeSBz dWNjZWVkcyB0byB0aGF0IGhvc3QuIFRoZSBkZWZhdWx0IGlzIDEwMC4KPiArCj4gK0BpdGVtIEBj b2Rle3F1ZXVlfSAoZGVmYXVsdDogQGNvZGV7I2Z9KQo+ICsKPiArQGNvZGV7cXVldWV9IGV4cGVj dHMgYW4gQGNvZGV7PG9wZW5zbXRwZC1xdWV1ZT59IHJlY29yZC4gV2l0aCBpdCwKPiBvbmUgbWF5 Cj4gK2NvbXByZXNzIGFuZCBlbmNyeXB0IHF1ZXVlLWVkIGVtYWlscyBhcyB3ZWxsIGFzIHNldCB0 aGUgZGVmYXVsdAo+IGV4cGlyYXRpb24KPiArdGltZSBmb3IgdGVtcG9yYXJpbHkgdW5kZWxpdmVy YWJsZSBtZXNzYWdlcy4KPiArCj4gK0BpdGVtIEBjb2Rle3NtdHB9IChkZWZhdWx0OiBAY29kZXsj Zn0pCj4gKwo+ICtAY29kZXtzbXRwfSBleHBlY3RzIGFuIEBjb2RlezxvcGVuc210cGQtc210cD59 IHJlY29yZCwgd2hpY2ggbGV0cwo+IG9uZQo+ICtzcGVjaWZpeSBob3cgbGFyZ2UgZW1haWwgbWF5 IGJlIGFsb25nIHdpdGggb3RoZXIgc2V0dGluZ3MuCj4gKwo+ICtAaXRlbSBAY29kZXtzcnN9IChk ZWZhdWx0OiBAY29kZXsjZn0pCj4gKwo+ICtAY29kZXtzcnN9IGV4cGVjdHMgYW4gQGNvZGV7PG9w ZW5zbXRwZC1zcnM+fSByZWNvcmQsIHdoaWNoIGxldHMgb25lCj4gc2V0Cj4gK3VwIFNSUywgdGhl IFNlbmRlciBSZXdyaXR0aW5nIFNjaGVtZS4KPiArPT09PT09PQo+IMKgQGl0ZW0gQGNvZGV7c2V0 Z2lkLWNvbW1hbmRzP30gKGRlZmF1bHQ6IEBjb2RleyN0fSkKPiDCoE1ha2UgdGhlIGZvbGxvd2lu ZyBjb21tYW5kcyBzZXRnaWQgdG8gQGNvZGV7c210cHF9IHNvIHRoZXkgY2FuIGJlCj4gwqBleGVj dXRlZDogQGNvbW1hbmR7c210cGN0bH0sIEBjb21tYW5ke3NlbmRtYWlsfSwgQGNvbW1hbmR7c2Vu ZC0KPiBtYWlsfSwKPiDCoEBjb21tYW5ke21ha2VtYXB9LCBAY29tbWFuZHttYWlscX0sIGFuZCBA Y29tbWFuZHtuZXdhbGlhc2VzfS4KPiDCoEB4cmVme1NldHVpZCBQcm9ncmFtc30sIGZvciBtb3Jl IGluZm9ybWF0aW9uIG9uIHNldGdpZCBwcm9ncmFtcy4KPiArPj4+Pj4+PiBvcmlnaW4vbWFzdGVy Cj4gwqBAZW5kIHRhYmxlCj4gwqBAZW5kIGRlZnRwCj4gwqAKPiArQGl0ZW1pemUKPiArQGl0ZW0K PiArRGF0YSBUeXBlOiBvcGVuc210cGQtaW50ZXJmYWNlCj4gKwo+ICtEYXRhIHR5cGUgcmVwcmVz ZW50aW5nIHRoZSBjb25maWd1cmF0aW9uIG9mIGFuCj4gK0Bjb2RlezxvcGVuc210cGQtaW50ZXJm YWNlPn0uIExpc3RlbiBvbiB0aGUgZmllbGRuYW1lCj4gQGNvZGV7aW50ZXJmYWNlfSBmb3IKPiAr aW5jb21pbmcgY29ubmVjdGlvbnMsIHVzaW5nIHRoZSBzYW1lIHN5bnRheCBhcyBmb3IgaWZjb25m aWcoOCkuIFRoZQo+IGludGVyZmFjZQo+ICtwYXJhbWV0ZXIgbWF5IGFsc28gYmUgYW4gc3RyaW5n IGludGVyZmFjZSBncm91cCwgYW4gc3RyaW5nIElQCj4gYWRkcmVzcywgb3IgYQo+ICtzdHJpbmcg ZG9tYWluIG5hbWUuIExpc3RlbmluZyBjYW4gb3B0aW9uYWxseSBiZSByZXN0cmljdGVkIHRvIGEK PiBzcGVjaWZpYwo+ICthZGRyZXNzIGZpZWxkbmFtZSBAY29kZXtmYW1pbHl9LCB3aGljaCBjYW4g YmUgZWl0aGVyIGBgaW5ldDQnJyBvcgo+IGBgaW5ldDYnJy4KPiArCj4gK0BpdGVtaXplCj4gK0Bp dGVtIEBjb2Rle2ludGVyZmFjZX0gKGRlZmF1bHQ6IGBgbG8nJykKPiArCj4gK1RoZSBzdHJpbmcg aW50ZXJmYWNlIHRvIGxpc3RlbiBmb3IgaW5jb21pbmcgY29ubmVjdGlvbnMuIFRoZXNlCj4gaW50 ZXJmYWNlIGNhbgo+ICt1c3VhbGx5IGJlIGZvdW5kIGJ5IHRoZSBjb21tYW5kIEBjb2Rle2lwIGxp bmt9Lgo+ICsKPiArQGl0ZW0gQGNvZGV7ZmFtaWx5fSAoZGVmYXVsdDogQGNvZGV7I2Z9KQo+ICsK PiArVGhlIHN0cmluZyBJUCBmYW1pbHkgdG8gdXNlLsKgIFZhbGlkIHN0cmluZ3MgYXJlIGBgaW5l dDQnJyBvcgo+IGBgaW5ldDYnJy4KPiArCj4gK0BpdGVtIEBjb2Rle2F1dGh9IChkZWZhdWx0OiBA Y29kZXsjZn0pCj4gKwo+ICtTdXBwb3J0IFNNVFBBVVRIOiBjbGllbnRzIG1heSBvbmx5IHN0YXJ0 IFNNVFAgdHJhbnNhY3Rpb25zIGFmdGVyCj4gc3VjY2Vzc2Z1bAo+ICthdXRoZW50aWNhdGlvbi4g SWYgQGNvZGV7YXV0aH0gaXMgQGNvZGV7I3R9LCB0aGVuIHVzZXJzIGFyZQo+IGF1dGhlbnRpY2F0 ZWQgYWdhaW5zdAo+ICt0aGVpciBvd24gbm9ybWFsIGxvZ2luIGNyZWRlbnRpYWxzLiBBbHRlcm5h dGl2ZWx5IEBjb2Rle2F1dGh9IG1heSBiZQo+IGFuCj4gK0Bjb2RlezxvcGVuc210cGQtdGFibGU+ fSB3aG9zZSB1c2VycyBhcmUgYXV0aGVudGljYXRlZCBhZ2FpbnN0Cj4gK3RoZWlyIHBhc3N3b3Jk cy4KPiArCj4gK0BpdGVtIEBjb2Rle2F1dGgtb3B0aW9uYWx9IChkZWZhdWx0OiBAY29kZXsjZn0p Cj4gKwo+ICtTdXBwb3J0IFNNVFBBVVRIIG9wdGlvbmFsbHk6IGNsaWVudHMgbmVlZCBub3QgYXV0 aGVudGljYXRlLCBidXQgbWF5Cj4gZG8gc28uCj4gK1RoaXMgYWxsb3dzIHRoZSBAY29kZXs8b3Bl bnNtdHBkLWludGVyZmFjZT59IHRvIGJvdGggYWNjZXB0Cj4gK2luY29taW5nIG1haWwgZnJvbSB1 bnRydXN0ZWQgc2VuZGVycyBhbmQgcGVybWl0IG91dGdvaW5nIG1haWwgZnJvbQo+ICthdXRoZW50 aWNhdGVkIHVzZXJzICh1c2luZyBAY29kZXs8b3BlbnNtdHBkLW1hdGNoPn0gZmllbGRuYW1lCj4g K0Bjb2Rle2F1dGh9KS4gSXQgY2FuIGJlIHVzZWQgaW4gc2l0dWF0aW9ucyB3aGVyZSBpdCBpcyBu b3QgcG9zc2libGUKPiB0byBsaXN0ZW4gb24KPiArYSBzZXBhcmF0ZSBwb3J0ICh1c3VhbGx5IHRo ZSBzdWJtaXNzaW9uIHBvcnQsIDU4NykgZm9yIHVzZXJzIHRvCj4gK2F1dGhlbnRpY2F0ZS4KPiAr Cj4gK0BpdGVtIEBjb2Rle2ZpbHRlcnN9IChkZWZhdWx0OiBAY29kZXsjZn0pCj4gKwo+ICtBIGxp c3Qgb2Ygb25lIG9yIG1hbnkgQGNvZGV7PG9wZW5zbXRwZC1maWx0ZXI+fSBvcgo+ICtAY29kZXs8 b3BlbnNtdHBkLWZpbHRlci1waGFzZT59IHJlY29yZHMuIFRoZSBmaWx0ZXJzIGFyZSBhcHBsaWVk Cj4gK3NlcXVlbnRpYWxseS4gVGhlc2UgcmVjb3JkcyBsaXN0ZW4gYW5kIGZpbHRlciBvbiBjb25u ZWN0aW9ucyBoYW5kbGVkCj4gYnkgdGhpcwo+ICtsaXN0ZW5lci4KPiArCj4gK0BpdGVtIEBjb2Rl e2hvc3RuYW1lfSAoZGVmYXVsdDogQGNvZGV7I2Z9KQo+ICsKPiArVXNlIHN0cmluZyBgYGhvc3Ru YW1lJycgaW4gdGhlIGdyZWV0aW5nIGJhbm5lciBpbnN0ZWFkIG9mIHRoZQo+IGRlZmF1bHQgc2Vy dmVyCj4gK25hbWUuCj4gKwo+ICtAaXRlbSBAY29kZXtob3N0bmFtZXN9IChkZWZhdWx0OiBAY29k ZXsjZn0pCj4gKwo+ICtPdmVycmlkZSB0aGUgc2VydmVyIG5hbWUgZm9yIHNwZWNpZmljIGFkZHJl c3Nlcy4gVXNlIGEKPiArQGNvZGV7PG9wZW5zbXRwZC10YWJsZT59IGNvbnRhaW5pbmcgYSBtYXBw aW5nIG9mIHN0cmluZyBJUAo+ICthZGRyZXNzZXMgdG8gaG9zdG5hbWVzLiBJZiB0aGUgYWRkcmVz cyBvbiB3aGljaCB0aGUgY29ubmVjdGlvbgo+IGFycml2ZXMKPiArYXBwZWFycyBpbiB0aGUgbWFw cGluZywgdGhlIGFzc29jaWF0ZWQgaG9zdG5hbWUgaXMgdXNlZC4KPiArCj4gK0BpdGVtIEBjb2Rl e21hc2stc3JjfSAoZGVmYXVsdDogQGNvZGV7I2Z9KQo+ICsKPiArSWYgQGNvZGV7I3R9LCB0aGVu IG9taXQgdGhlIGZyb20gcGFydCB3aGVuIHByZXBlbmRpbmcg4oCcUmVjZWl2ZWTigJ0KPiBoZWFk ZXJzLgo+ICsKPiArQGl0ZW0gQGNvZGV7ZGlzYWJsZS1kc259IChkZWZhdWx0OiBAY29kZXsjZn0p Cj4gKwo+ICtXaGVuIEBjb2RleyN0fSwgdGhlbiBkaXNhYmxlIHRoZSBEU04gKERlbGl2ZXJ5IFN0 YXR1cyBOb3RpZmljYXRpb24pCj4gZXh0ZW5zaW9uLgo+ICsKPiArQGl0ZW0gQGNvZGV7cGtpfSAo ZGVmYXVsdDogQGNvZGV7I2Z9KQo+ICsKPiArRm9yIHNlY3VyZSBjb25uZWN0aW9ucywgdXNlIGFu IEBjb2RlezxvcGVuc210cGQtcGtpPn0KPiArdG8gcHJvdmUgYSBtYWlsIHNlcnZlcidzIGlkZW50 aXR5Lgo+ICsKPiArQGl0ZW0gQGNvZGV7cG9ydH0gKGRlZmF1bHQ6IEBjb2RleyNmfSkKPiArCj4g K0xpc3RlbiBvbiB0aGUgaW50ZWdlciBwb3J0IGluc3RlYWQgb2YgdGhlIGRlZmF1bHQgcG9ydCBv ZiAyNS4KPiArCj4gK0BpdGVtIEBjb2Rle3Byb3h5LXYyfSAoZGVmYXVsdDogQGNvZGV7I2Z9KQo+ ICsKPiArSWYgQGNvZGV7I3R9LCB0aGVuIHN1cHBvcnQgdGhlIFBST1hZdjIgcHJvdG9jb2wsIHJl d3JpdGluZwo+IGFwcHJvcHJpYXRlbHkgc291cmNlCj4gK2FkZHJlc3MgcmVjZWl2ZWQgZnJvbSBw cm94eS4KPiArCj4gK0BpdGVtIEBjb2Rle3JlY2VpdmVkLWF1dGh9IChkZWZhdWx0OiBAY29kZXsj Zn0pCj4gKwo+ICtJZiBAY29kZXsjdH0sIHRoZW4gaW4g4oCcUmVjZWl2ZWTigJ0gaGVhZGVycywg cmVwb3J0IHdoZXRoZXIgdGhlIHNlc3Npb24KPiB3YXMKPiArYXV0aGVudGljYXRlZCBhbmQgYnkg d2hpY2ggbG9jYWwgdXNlci4KPiArCj4gK0BpdGVtIEBjb2Rle3NlbmRlcnN9IChkZWZhdWx0OiBA Y29kZXsjZn0pCj4gKwo+ICtMb29rIHVwIHRoZSBhdXRoZW50aWNhdGVkIHVzZXIgaW4gdGhlIHN1 cHBsaWVkCj4gK0Bjb2RlezxvcGVuc210cGQtdGFibGU+fSB0byBmaW5kIHRoZSBlbWFpbCBhZGRy ZXNzZXMgdGhhdCB1c2VyIGlzCj4gK2FsbG93ZWQgdG8gc3VibWl0IG1haWwgYXMuCj4gKwo+ICtA aXRlbSBAY29kZXtzZWN1cmUtY29ubmVjdGlvbn0gKGRlZmF1bHQ6IEBjb2RleyNmfSkKPiArCj4g K1RoaXMgaXMgYSBzdHJpbmcgb2Ygb25lIG9mIHRoZXNlIG9wdGlvbnM6Cj4gKwo+ICtAbXVsdGl0 YWJsZSB7YWFhYWFhYWFhYWFhYWFhYWFhYWF9Cj4ge2FhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh YWFhYWFhYWFhYWFhYWFhYWF9Cj4gK0BpdGVtIGBgc210cHMnJwo+ICtAdGFiIFN1cHBvcnQgU01U UFMsIGJ5IGRlZmF1bHQgb24gcG9ydCA0NjUuCj4gK0BpdGVtIGBgdGxzJycKPiArQHRhYiBTdXBw b3J0IFNUQVJUVExTLCBieSBkZWZhdWx0IG9uIHBvcnQgMjUuCj4gK0BpdGVtIGBgdGxzLXJlcXVp cmUtdmVyaWZ5JycKPiArQHRhYiBMaWtlIHRscywgYnV0IGZvcmNlIGNsaWVudHMgdG8gZXN0YWJs aXNoCj4gK0BpdGVtCj4gK0B0YWIgYSBzZWN1cmUgY29ubmVjdGlvbiBiZWZvcmUgYmVpbmcgYWxs b3dlZCB0bwo+ICtAaXRlbQo+ICtAdGFiIHN0YXJ0IGFuIFNNVFAgdHJhbnNhY3Rpb24uwqAgV2l0 aCB0aGUgdmVyaWZ5Cj4gK0BpdGVtCj4gK0B0YWIgb3B0aW9uLCBjbGllbnRzIG11c3QgYWxzbyBw cm92aWRlIGEgdmFsaWQKPiArQGl0ZW0KPiArQHRhYiBjZXJ0aWZpY2F0ZSB0byBlc3RhYmxpc2gg YW4gU01UUCBzZXNzaW9uLgo+ICtAZW5kIG11bHRpdGFibGUKPiArCj4gK0BpdGVtIEBjb2Rle3Rh Z30gKGRlZmF1bHQ6IEBjb2RleyNmfSkKPiArCj4gK0NsaWVudHMgY29ubmVjdGluZyB0byB0aGUg bGlzdGVuZXIgYXJlIHRhZ2dlZCB3aXRoIHRoZSBnaXZlbiBzdHJpbmcKPiB0YWcuCj4gK0BlbmQg aXRlbWl6ZQo+ICsKPiArQGl0ZW0gRGF0YSBUeXBlOiBvcGVuc210cGQtc29ja2V0Cj4gKwo+ICtE YXRhIHR5cGUgcmVwcmVzZW50aW5nIHRoZSBjb25maWd1cmF0aW9uIG9mIGFuCj4gK0Bjb2Rlezxv cGVuc210cGQtc29ja2V0Pn0uIExpc3RlbiBmb3IgaW5jb21pbmcgU01UUAo+ICtjb25uZWN0aW9u cyBvbiB0aGUgVW5peCBkb21haW4gc29ja2V0IEBzYW1wey92YXIvcnVuL3NtdHBkLnNvY2t9Lgo+ IFRoaXMgaXMgZG9uZSBieQo+ICtkZWZhdWx0LCBldmVuIGlmIHRoZSBkaXJlY3RpdmUgaXMgYWJz ZW50Lgo+ICsKPiArQGl0ZW1pemUKPiArQGl0ZW0gQGNvZGV7ZmlsdGVyc30gKGRlZmF1bHQ6IEBj b2RleyNmfSkKPiArCj4gK0EgbGlzdCBvZiBvbmUgb3IgbWFueSBAY29kZXs8b3BlbnNtdHBkLWZp bHRlcj59IG9yCj4gK0Bjb2RlezxvcGVuc210cGQtZmlsdGVyLXBoYXNlPn0gcmVjb3Jkcy4gVGhl c2UgZmlsdGVyIGluY29taW5nCj4gK2Nvbm5lY3Rpb25zIGhhbmRsZWQgYnkgdGhpcyBsaXN0ZW5l ci4KPiArCj4gK0BpdGVtIEBjb2Rle21hc2stc3JjfSAoZGVmYXVsdDogQGNvZGV7I2Z9KQo+ICsK PiArSWYgQGNvZGV7I3R9LCB0aGVuIG9taXQgdGhlIGZyb20gcGFydCB3aGVuIHByZXBlbmRpbmcg 4oCcUmVjZWl2ZWTigJ0KPiBoZWFkZXJzLgo+ICsKPiArQGl0ZW0gQGNvZGV7dGFnfSAoZGVmYXVs dDogQGNvZGV7I2Z9KQo+ICsKPiArQ2xpZW50cyBjb25uZWN0aW5nIHRvIHRoZSBsaXN0ZW5lciBh cmUgdGFnZ2VkIHdpdGggdGhlIGdpdmVuIHN0cmluZwo+IHRhZy4KPiArQGVuZCBpdGVtaXplCj4g Kwo+ICtAaXRlbSBEYXRhIFR5cGU6IG9wZW5zbXRwZC1tYXRjaAo+ICsKPiArVGhpcyBkYXRhIHR5 cGUgcmVwcmVzZW50cyB0aGUgY29uZmlndXJhdGlvbiBvZiBhbgo+ICtAY29kZXs8b3BlbnNtdHBk LW1hdGNoPn0gcmVjb3JkLgo+ICsKPiArSWYgYXQgbGVhc3Qgb25lIG1haWwgZW52ZWxvcGUgbWF0 Y2hlcyB0aGUgb3B0aW9ucyBvZiBvbmUgbWF0Y2gKPiByZWNvcmQsIHJlY2VpdmUKPiArdGhlIGlu Y29taW5nIG1lc3NhZ2UsIHB1dCBhIGNvcHkgaW50byBlYWNoIG1hdGNoaW5nIGVudmVsb3BlLCBh bmQKPiBhdG9taWNhbGx5Cj4gK3NhdmUgdGhlIGVudmVsb3BlcyB0byB0aGUgbWFpbCBzcG9vbCBm b3IgbGF0ZXIgcHJvY2Vzc2luZyBieSB0aGUKPiByZXNwZWN0aXZlCj4gK0Bjb2RlezxvcGVuc210 cGQtYWN0aW9uPn0gZm91bmQgaW4gZmllbGRuYW1lIEBjb2Rle2FjdGlvbn0uCj4gKwo+ICtAaXRl bWl6ZQo+ICtAaXRlbSBAY29kZXthY3Rpb259IChkZWZhdWx0OiBAY29kZXsjZn0pCj4gKwo+ICtJ ZiBtYWlsIG1hdGNoZXMgdGhpcyBtYXRjaCBjb25maWd1cmF0aW9uLCB0aGVuIGRvIHRoaXMgYWN0 aW9uLiBWYWxpZAo+IHZhbHVlcwo+ICtpbmNsdWRlIEBjb2RlezxvcGVuc210cGQtbG9jYWwtZGVs aXZlcnk+fSBvcgo+ICtAY29kZXs8b3BlbnNtdHBkLXJlbGF5Pn0uCj4gKwo+ICtAaXRlbSBAY29k ZXtvcHRpb25zfSAoZGVmYXVsdDogQGNvZGV7I2Z9KSBAY29kZXs8b3BlbnNtdHBkLW9wdGlvbj59 Cj4gK1RoZSBmaWVsZG5hbWUgJ29wdGlvbicgaXMgYSBsaXN0IG9mIHVuaXF1ZQo+ICtAY29kZXs8 b3BlbnNtdHBkLW9wdGlvbj59IHJlY29yZHMuCj4gKwo+ICtFYWNoIEBjb2RlezxvcGVuc210cGQt b3B0aW9uPn0gcmVjb3JkJ3MgZmllbGRuYW1lICdvcHRpb24nIGhhcyBzb21lCj4gK211dHVhbGx5 IGV4Y2x1c2l2ZSBvcHRpb25zOiB0aGVyZSBjYW4gYmUgb25seSBvbmUgYGBmb3InJyBhbmQgb25s eQo+IG9uZSBgYGZyb20nJyBvcHRpb24uCj4gKwo+ICtAbXVsdGl0YWJsZSB7YWFhYWFhYWFhYWFh YWFhYWFhYWFhYWFhYX0KPiB7YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhfQo+ICtAaGVh ZGl0ZW0gZm9yCj4gK0B0YWIgZnJvbQo+ICtAaXRlbSBvbmx5IHVzZSBvbmUgb2YgdGhlIGZvbGxv d2luZzoKPiArQHRhYiBvbmx5IHVzZSBvbmUgb2YgdGhlIGZvbGxvd2luZzoKPiArQGl0ZW0gYGBm b3IgYW55JycKPiArQHRhYiBgYGZyb20gYW55JycKPiArQGl0ZW0gYGBmb3IgbG9jYWwnJwo+ICtA dGFiIGBgZnJvbSBhdXRoJycKPiArQGl0ZW0gYGBmb3IgZG9tYWluJycKPiArQHRhYiBgYGZyb20g bG9jYWwnJwo+ICtAaXRlbSBgYGZvciByY3B0LXRvJycKPiArQHRhYiBgYGZyb20gbWFpbC1mcm9t JycKPiArQGl0ZW0KPiArQHRhYiBgYGZyb20gc29ja2V0JycKPiArQGl0ZW0KPiArQHRhYiBgYGZy b20gc3JjJycKPiArQGVuZCBtdWx0aXRhYmxlCj4gKwo+ICtUaGUgZm9sbG93aW5nIG1hdGNoaW5n IG9wdGlvbnMgYXJlIHN1cHBvcnRlZCBhbmQgY2FuIGFsbCBiZSBuZWdhdGVkCj4gKHZpYSBub3QK PiArI3QpLiBUaGUgb3B0aW9ucyB0aGF0IHN1cHBvcnQgYSB0YWJsZSAoYW55dGhpbmcgc3Vycm91 bmRlZCB3aXRoICc8Jwo+IGFuZCAnPicKPiArZWc6IDx0YWJsZT4pLCBhbHNvIHN1cHBvcnQgc3Bl Y2lmeWluZyByZWdleCB2aWEgKHJlZ2V4ICN0KS4KPiArCj4gK0BpdGVtaXplCj4gK0BpdGVtIEBz YW1we2ZvciBhbnl9Cj4gKwo+ICtTcGVjaWZ5IHRoYXQgc2Vzc2lvbiBtYXkgYWRkcmVzcyBhbnkg ZGVzdGluYXRpb24uCj4gKwo+ICtAaXRlbSBAc2FtcHtmb3IgbG9jYWx9Cj4gKwo+ICtTcGVjaWZ5 IHRoYXQgc2Vzc2lvbiBtYXkgYWRkcmVzcyBhbnkgbG9jYWwgZG9tYWluLsKgIFRoaXMgaXMgdGhl Cj4gZGVmYXVsdCwKPiArYW5kIG1heSBiZSBvbWl0dGVkLgo+ICsKPiArQGl0ZW0gQHNhbXB7Zm9y IGRvbWFpbiBfZG9tYWluXyB8IDxkb21haW4+fQo+ICsKPiArU3BlY2lmeSB0aGF0IHNlc3Npb24g bWF5IGFkZHJlc3MgdGhlIHN0cmluZyBvciBsaXN0IHRhYmxlIGRvbWFpbi4KPiArCj4gK0BpdGVt IEBzYW1we2ZvciByY3B0LXRvIF9yZWNpcGllbnRfIHwgPHJlY2lwaWVudD59Cj4gKwo+ICtTcGVj aWZ5IHRoYXQgc2Vzc2lvbiBtYXkgYWRkcmVzcyB0aGUgc3RyaW5nIG9yIGxpc3QgdGFibGUgcmVj aXBpZW50Lgo+ICsKPiArQGl0ZW0gQHNhbXB7ZnJvbSBhbnl9Cj4gKwo+ICtTcGVjaWZ5IHRoYXQg c2Vzc2lvbiBtYXkgb3JpZ2luYXRlIGZyb20gYW55IHNvdXJjZS4KPiArCj4gK0BpdGVtIEBzYW1w e2Zyb20gYXV0aH0KPiArCj4gK1NwZWNpZnkgdGhhdCBzZXNzaW9uIG1heSBvcmlnaW5hdGUgZnJv bSBhbnkgYXV0aGVudGljYXRlZCB1c2VyLCBubwo+IG1hdHRlcgo+ICt0aGUgc291cmNlIElQIGFk ZHJlc3MuCj4gKwo+ICtAaXRlbSBAc2FtcHtmcm9tIGF1dGggX3VzZXJfIHwgPHVzZXI+fQo+ICsK PiArU3BlY2lmeSB0aGF0IHNlc3Npb24gbWF5IG9yaWdpbmF0ZSBmcm9tIGF1dGhlbnRpY2F0ZWQg dXNlciBvciB1c2VyCj4gbGlzdAo+ICt1c2VyLCBubyBtYXR0ZXIgdGhlIHNvdXJjZSBJUCBhZGRy ZXNzLgo+ICsKPiArQGl0ZW0gQHNhbXB7ZnJvbSBsb2NhbH0KPiArCj4gK1NwZWNpZnkgdGhhdCBz ZXNzaW9uIG1heSBvbmx5IG9yaWdpbmF0ZSBmcm9tIGEgbG9jYWwgSVAgYWRkcmVzcywgb3IKPiBm cm9tCj4gK3RoZSBsb2NhbCBlbnF1ZXVlci7CoCBUaGlzIGlzIHRoZSBkZWZhdWx0LCBhbmQgbWF5 IGJlIG9taXR0ZWQuCj4gKwo+ICtAaXRlbSBAc2FtcHtmcm9tIG1haWwtZnJvbSBfc2VuZGVyXyB8 IDxzZW5kZXI+fQo+ICsKPiArU3BlY2lmeSB0aGF0IHNlc3Npb24gbWF5IG9yaWdpbmF0ZSBmcm9t IHNlbmRlciBvciB0YWJsZSBzZW5kZXIsIG5vCj4gK21hdHRlciB0aGUgc291cmNlIElQIGFkZHJl c3MuCj4gKwo+ICtAaXRlbSBAc2FtcHtmcm9tIHJkbnN9Cj4gKwo+ICtTcGVjaWZ5IHRoYXQgc2Vz c2lvbiBtYXkgb25seSBvcmlnaW5hdGUgZnJvbSBhbiBJUCBhZGRyZXNzIHRoYXQKPiByZXNvbHZl cwo+ICt0byBhIHJldmVyc2UgRE5TQC4KPiArCj4gK0BpdGVtIEBzYW1we2Zyb20gcmRucyBfaG9z dG5hbWVfIHwgPGhvc3RuYW1lPn0KPiArCj4gK1NwZWNpZnkgdGhhdCBzZXNzaW9uIG1heSBvbmx5 IG9yaWdpbmF0ZSBmcm9tIGFuIElQIGFkZHJlc3MgdGhhdAo+IHJlc29sdmVzCj4gK3RvIGEgcmV2 ZXJzZSBETlMgbWF0Y2hpbmcgc3RyaW5nIG9yIGxpc3Qgc3RyaW5nIGhvc3RuYW1lLgo+ICsKPiAr QGl0ZW0gQHNhbXB7ZnJvbSBzb2NrZXR9Cj4gKwo+ICtTcGVjaWZ5IHRoYXQgc2Vzc2lvbiBtYXkg b25seSBvcmlnaW5hdGUgZnJvbSB0aGUgbG9jYWwgZW5xdWV1ZXIuCj4gKwo+ICtAaXRlbSBAc2Ft cHtmcm9tIHNyYyBfYWRkcmVzc18gfCA8YWRkcmVzcz59Cj4gKwo+ICtTcGVjaWZ5IHRoYXQgc2Vz c2lvbiBtYXkgb25seSBvcmlnaW5hdGUgZnJvbSBzdHJpbmcgb3IgbGlzdCB0YWJsZQo+IGFkZHJl c3MKPiArd2hpY2ggY2FuIGJlIGEgc3BlY2lmaWMgYWRkcmVzcyBvciBhIHN1Ym5ldCBleHByZXNz ZWQgaW4gQ0lEUi0KPiBub3RhdGlvbi4KPiArCj4gK0BpdGVtIEBzYW1we2F1dGh9Cj4gKwo+ICtN YXRjaGVzIHRyYW5zYWN0aW9ucyB3aGljaCBoYXZlIGJlZW4gYXV0aGVudGljYXRlZC4KPiArCj4g K0BpdGVtIEBzYW1we2F1dGggX3VzZXJuYW1lXyB8IDx1c2VybmFtZT59Cj4gKwo+ICtNYXRjaGVz IHRyYW5zYWN0aW9ucyB3aGljaCBoYXZlIGJlZW4gYXV0aGVudGljYXRlZCBmb3IgdXNlciBvciB1 c2VyCj4gbGlzdAo+ICt1c2VybmFtZS4KPiArCj4gK0BpdGVtIEBzYW1we2hlbG8gX2hlbG8tbmFt ZV8gfCA8aGVsby1uYW1lPn0KPiArCj4gK1NwZWNpZnkgdGhhdCBzZXNzaW9uJ3MgSEVMTyAvIEVI TE8gc2hvdWxkIG1hdGNoIHRoZSBzdHJpbmcgb3IgbGlzdAo+IHRhYmxlCj4gK2hlbG8tbmFtZS4K PiArCj4gK0BpdGVtIEBzYW1we21haWwtZnJvbSBfc2VuZGVyXyB8IDxzZW5kZXI+fQo+ICsKPiAr U3BlY2lmeSB0aGF0IHRyYW5zYWN0aW9ucydzIE1BSUwgRlJPTSBzaG91bGQgbWF0Y2ggdGhlIHN0 cmluZyBvcgo+IGxpc3QKPiArdGFibGUgc2VuZGVyLgo+ICsKPiArQGl0ZW0gQHNhbXB7cmNwdC10 byBfcmVjaXBpZW50XyB8IDxyZWNpcGllbnQ+fQo+ICsKPiArU3BlY2lmeSB0aGF0IHRyYW5zYWN0 aW9uJ3MgUkNQVCBUTyBzaG91bGQgbWF0Y2ggdGhlIHN0cmluZyBvciBsaXN0Cj4gdGFibGUKPiAr cmVjaXBpZW50Lgo+ICsKPiArQGl0ZW0gQHNhbXB7dGFnIHRhZ30KPiArTWF0Y2hlcyB0cmFuc2Fj dGlvbnMgdGFnZ2VkIHdpdGggdGhlIGdpdmVuIHRhZy4KPiArCj4gK0BpdGVtIEBzYW1we3Rsc30K PiArU3BlY2lmeSB0aGF0IHRyYW5zYWN0aW9uIHNob3VsZCB0YWtlIHBsYWNlIGluIGEgVExTIGNo YW5uZWwuCj4gK0BlbmQgaXRlbWl6ZQo+ICsKPiArSGVyZSBpcyBhIHNpbXBsZSBleGFtcGxlOgo+ ICtAbGlzcAo+ICvCoMKgwqAgKG9wZW5zbXRwZC1vcHRpb24KPiArwqDCoMKgwqAgKG5vdCAjdCkK PiArwqDCoMKgwqAgKHJlZ2V4ICNmKQo+ICvCoMKgwqDCoCAob3B0aW9uICJmb3IgZG9tYWluIikK PiArwqDCoMKgwqAgKGRhdGEgKG9wZW5zbXRwZC10YWJsZQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIChuYW1lICJkb21haW4tdGFibGUiKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChkYXRh IChsaXN0ICJnbnUub3JnIiAiZGlzbWFpbC5kZSIpKSkpKQo+ICtAZW5kIGxpc3AKPiArCj4gK1Ro ZSBtYWlsIG11c3QgTk9UIGNvbWUgZnJvbSB0aGUgZG9tYWlucyBAc2FtcHtnbnUub3JnfSBvcgo+ IEBzYW1we2Rpc21haWwuZGV9Lgo+ICsKPiArQGl0ZW0gRGF0YSBUeXBlOiBvcGVuc210cGQtb3B0 aW9uCj4gK0BlbmQgaXRlbWl6ZQo+ICsKPiArQGl0ZW0gRGF0YSBUeXBlOiBvcGVuc210cGQtbG9j YWwtZGVsaXZlcnkKPiArCj4gK1RoaXMgZGF0YSB0eXBlIHJlcHJlc2VudHMgdGhlIGNvbmZpZ3Vy YXRpb24gb2YgYW4KPiArQGNvZGV7PG9wZW5zbXRwZC1sb2NhbC1kZWxpdmVyeT59IHJlY29yZC4K PiArCj4gK0BpdGVtaXplCj4gK0BpdGVtCj4gK0Bjb2Rle25hbWV9IChkZWZhdWx0OiBAY29kZXsj Zn0pCj4gKwo+ICtAY29kZXtuYW1lfSBpcyB0aGUgc3RyaW5nIG5hbWUgb2YgdGhlIHJlbGF5IGFj dGlvbi4KPiArCj4gK0BpdGVtIEBjb2Rle21ldGhvZH0gKGRlZmF1bHQ6IEBjb2RleyJtYm94In0p Cj4gKwo+ICtUaGUgZW1haWwgZGVsaXZlcnkgb3B0aW9uLsKgIFZhbGlkIG9wdGlvbnMgYXJlOgo+ ICsKPiArQGl0ZW1pemUKPiArQGl0ZW0gQGNvZGV7Im1ib3gifQo+ICsKPiArRGVsaXZlciB0aGUg bWVzc2FnZSB0byB0aGUgdXNlcidzIG1ib3ggd2l0aCBtYWlsLmxvY2FsKDgpLgo+ICsKPiArQGl0 ZW0gQGNvZGV7ImV4cGFuZC1vbmx5In0KPiArCj4gK09ubHkgYWNjZXB0IHRoZSBtZXNzYWdlIGlm IGEgZGVsaXZlcnkgbWV0aG9kIHdhcyBzcGVjaWZpZWQgaW4gYW4KPiBhbGlhc2VzCj4gK29yIC5m b3J3YXJkIGZpbGUuCj4gKwo+ICtAaXRlbSBAY29kZXsiZm9yd2FyZC1vbmx5In0KPiArCj4gK09u bHkgYWNjZXB0IHRoZSBtZXNzYWdlIGlmIHRoZSByZWNpcGllbnQgcmVzdWx0cyBpbiBhIHJlbW90 ZSBhZGRyZXNzCj4gYWZ0ZXIKPiArdGhlIHByb2Nlc3Npbmcgb2YgYWxpYXNlcyBvciBmb3J3YXJk IGZpbGUuCj4gKwo+ICtAaXRlbSBAY29kZXs8b3BlbnNtdHBkLWxtdHA+fQo+ICsKPiArRGVsaXZl ciB0aGUgbWVzc2FnZSB0byBhbiBMTVRQIHNlcnZlciBhdAo+ICtAY29kZXs8b3BlbnNtdHBkLWxt dHA+fSdzIGZpZWxkbmFtZSBAY29kZXtkZXN0aW5hdGlvbn0uIFRoZSBsb2NhdGlvbgo+ICttYXkg YmUgZXhwcmVzc2VkIGFzIHN0cmluZyBob3N0OnBvcnQgb3IgYXMgYSBVTklYIHNvY2tldC4KPiBP cHRpb25hbGx5LAo+ICtAY29kZXs8b3BlbnNtdHBkLWxtdHBvbmZpZ3VyYXRpb24+fSdzIGZpZWxk bmFtZSBAY29kZXtyY3B0LXRvfSBtaWdodAo+IGJlIHNwZWNpZmllZAo+ICt0byB1c2UgdGhlIHJl Y2lwaWVudCBlbWFpbCBhZGRyZXNzIChhZnRlciBleHBhbnNpb24pIGluc3RlYWQgb2YgdGhlCj4g bG9jYWwKPiArdXNlciBpbiB0aGUgTE1UUCBzZXNzaW9uIGFzIFJDUFQgVE9ALgo+ICsKPiArQGl0 ZW0gQGNvZGV7PG9wZW5zbXRwZC1tYWlsZGlyPn0KPiArCj4gK0RlbGl2ZXIgdGhlIG1lc3NhZ2Ug dG8gdGhlIG1haWxkaXIgaW4KPiArQGNvZGV7PG9wZW5zbXRwZC1tYWlsZGlyPn0ncyBmaWVsZG5h bWUgQGNvZGV7cGF0aG5hbWV9IGlmIHNwZWNpZmllZCwKPiArb3IgYnkgZGVmYXVsdCB0byBAc2Ft cHt+L01haWxkaXJ9Lgo+ICsKPiArVGhlIHBhdGhuYW1lIG1heSBjb250YWluIGZvcm1hdCBzcGVj aWZpZXJzIHRoYXQgYXJlIGV4cGFuZGVkIGJlZm9yZQo+IHVzZQo+ICsoc2VlIHRoZSBiZWxvdyBz ZWN0aW9uIGFib3V0IEZvcm1hdCBTcGVjaWZpZXJzKS4KPiArCj4gK0lmIEBjb2RlezxvcGVuc210 cGQtbWFpbGRpcj59J3MgcmVjb3JkIGZpZWxkbmFtZSBAY29kZXtqdW5rfSBpcwo+IEBjb2RleyN0 fSwKPiArdGhlbiBtZXNzYWdlIHdpbGwgYmUgbW92ZWQgdG8gdGhlIOKAmEp1bmvigJkgZm9sZGVy IGlmIGl0IGNvbnRhaW5zIGEKPiBwb3NpdGl2ZQo+ICvigJhYLVNwYW3igJkgaGVhZGVyLiBUaGlz IGZvbGRlciB3aWxsIGJlIGNyZWF0ZWQgdW5kZXIgZmllbGRuYW1lCj4gQGNvZGV7cGF0aG5hbWV9 IGlmCj4gK2l0IGRvZXMgbm90IHlldCBleGlzdC4KPiArCj4gK0BpdGVtIEBjb2RlezxvcGVuc210 cGQtbWRhPn0KPiArCj4gK0RlbGVnYXRlIHRoZSBkZWxpdmVyeSB0byB0aGUgQGNvZGV7PG9wZW5z bXRwZC1tZGE+fSdzIGZpZWxkbmFtZQo+ICtAY29kZXtjb21tYW5kfSAodHlwZSBzdHJpbmcpIHRo YXQgcmVjZWl2ZXMgdGhlIG1lc3NhZ2Ugb24gaXRzCj4gc3RhbmRhcmQgaW5wdXQuCj4gKwo+ICtU aGUgQGNvZGV7Y29tbWFuZH0gbWF5IGNvbnRhaW4gZm9ybWF0IHNwZWNpZmllcnMgdGhhdCBhcmUg ZXhwYW5kZWQKPiBiZWZvcmUgdXNlCj4gKyhzZWUgRm9ybWF0IFNwZWNpZmllcnMpLgo+ICtAZW5k IGl0ZW1pemUKPiArCj4gK0BpdGVtIEBjb2Rle2FsaWFzfSAoZGVmYXVsdDogQGNvZGV7I2Z9KQo+ ICsKPiArVXNlIHRoZSBtYXBwaW5nIHRhYmxlIGZvciBhbGlhc2VzIGV4cGFuc2lvbi4gQGNvZGV7 YWxpYXN9IGlzIGFuCj4gK0Bjb2RlezxvcGVuc210cGQtdGFibGU+fS4KPiArCj4gK0BpdGVtIEBj b2Rle3R0bH0gKGRlZmF1bHQ6IEBjb2RleyNmfSkKPiArCj4gK0Bjb2Rle3R0bH0gaXMgYSBzdHJp bmcgc3BlY2lmeSBob3cgbG9uZyBhIG1lc3NhZ2UgbWF5IHJlbWFpbiBpbiB0aGUKPiBxdWV1ZS7C oCBJdCdzCj4gK2Zvcm1hdCBpcyBAc2FtcHtuQHtzfG18aHxkQH19LsKgIGVnOiBgYDRtJycgaXMg Zm91ciBtaW51dGVzLgo+ICsKPiArQGl0ZW0gQGNvZGV7dXNlcn0gKGRlZmF1bHQ6IEBjb2RleyNm fSApCj4gKwo+ICtAY29kZXt1c2VyfSBpcyB0aGUgc3RyaW5nIHVzZXJuYW1lIGZvciBwZXJmb3Jt aW5nIHRoZSBkZWxpdmVyeSwgdG8KPiBiZSBsb29rZWQgdXAKPiArd2l0aCBnZXRwd25hbSgzKS4K PiArCj4gK1RoaXMgaXMgdXNlZCBmb3IgdmlydHVhbCBob3N0aW5nIHdoZXJlIGEgc2luZ2xlIHVz ZXJuYW1lIGlzIGluCj4gY2hhcmdlIG9mCj4gK2hhbmRsaW5nIGRlbGl2ZXJ5IGZvciBhbGwgdmly dHVhbCB1c2Vycy4KPiArCj4gK1RoaXMgb3B0aW9uIGlzIG5vdCB1c2FibGUgd2l0aCB0aGUgbWJv eCBkZWxpdmVyeSBtZXRob2QuCj4gKwo+ICtAaXRlbSBAY29kZXt1c2VyYmFzZX0gKGRlZmF1bHQ6 IEBjb2RleyNmfSkKPiArCj4gK0Bjb2Rle3VzZXJiYXNlfSBpcyBhbiBAY29kZXs8b3BlbnNtdHBk LXRhYmxlPn0gcmVjb3JkIGZvciBtYXBwaW5nCj4gdXNlcgo+ICtsb29rdXBzIGluc3RlYWQgb2Yg dGhlIGdldHB3bmFtKDMpIGZ1bmN0aW9uLgo+ICsKPiArVGhlIGZpZWxkbmFtZXMgQGNvZGV7dXNl cn0gYW5kIEBjb2Rle3VzZXJiYXNlfSBhcmUgbXV0dWFsbHkKPiBleGNsdXNpdmUuCj4gKwo+ICtA aXRlbSBAY29kZXt2aXJ0dWFsfSAoZGVmYXVsdDogQGNvZGV7I2Z9KQo+ICsKPiArQGNvZGV7dmly dHVhbH0gaXMgYW4gQGNvZGV7PG9wZW5zbXRwZC10YWJsZT59IHJlY29yZCBpcyB1c2VkIGZvcgo+ IHZpcnR1YWwKPiArZXhwYW5zaW9uLgo+ICtAZW5kIGl0ZW1pemUKPiArCj4gK0BpdGVtIERhdGEg VHlwZTogb3BlbnNtdHBkLXJlbGF5Cj4gKwo+ICtUaGlzIGRhdGEgdHlwZSByZXByZXNlbnRzIHRo ZSBjb25maWd1cmF0aW9uIG9mIGFuCj4gK0Bjb2RlezxvcGVuc210cGQtcmVsYXk+fSByZWNvcmQu Cj4gKwo+ICtAaXRlbWl6ZQo+ICtAaXRlbSBAY29kZXtuYW1lfSAoZGVmYXVsdDogQGNvZGV7I2Z9 KQo+ICsKPiArQGNvZGV7bmFtZX0gaXMgdGhlIHN0cmluZyBuYW1lIG9mIHRoZSByZWxheSBhY3Rp b24uCj4gKwo+ICtAaXRlbSBAY29kZXtiYWNrdXB9IChkZWZhdWx0OiBAY29kZXsjZn0pCj4gKwo+ ICtXaGVuIEBjb2RleyN0fSwgb3BlcmF0ZSBhcyBhIGJhY2t1cCBtYWlsIGV4Y2hhbmdlciBkZWxp dmVyaW5nCj4gbWVzc2FnZXMgdG8gYW55Cj4gK21haWwgZXhjaGFuZ2VyIHdpdGggaGlnaGVyIHBy aW9yaXR5Lgo+ICsKPiArQGl0ZW0gQGNvZGV7YmFja3VwLW14fSAoZGVmYXVsdDogQGNvZGV7I2Z9 KQo+ICsKPiArT3BlcmF0ZSBhcyBhIGJhY2t1cCBtYWlsIGV4Y2hhbmdlciBkZWxpdmVyaW5nIG1l c3NhZ2VzIHRvIGFueSBtYWlsCj4gZXhjaGFuZ2VyCj4gK3dpdGggaGlnaGVyIHByaW9yaXR5IHRo YW4gbWFpbCBleGNoYW5nZXIgaWRlbnRpZmllZCBhcyBzdHJpbmcgbmFtZS4KPiArCj4gK0BpdGVt IEBjb2Rle2hlbG99IChkZWZhdWx0OiBAY29kZXsjZn0pCj4gKwo+ICtBZHZlcnRpc2Ugc3RyaW5n IGhlbG9uYW1lIGFzIHRoZSBob3N0bmFtZSB0byBvdGhlciBtYWlsIGV4Y2hhbmdlcnMKPiBkdXJp bmcKPiArdGhlIEhFTE8gcGhhc2UuCj4gKwo+ICtAaXRlbSBAY29kZXtoZWxvLXNyY30gKGRlZmF1 bHQ6IEBjb2RleyNmfSApCj4gKwo+ICsgVXNlIHRoZSBtYXBwaW5nIEBjb2RlezxvcGVuc210cGQt dGFibGU+fSB0byBsb29rIHVwIGEgaG9zdG5hbWUKPiArbWF0Y2hpbmcgdGhlIHNvdXJjZSBhZGRy ZXNzLCB0byBhZHZlcnRpc2UgZHVyaW5nIHRoZSBIRUxPIHBoYXNlLgo+ICsKPiArQGl0ZW0gQGNv ZGV7ZG9tYWlufSAoZGVmYXVsdDogQGNvZGV7I2Z9KQo+ICsKPiArRG8gbm90IHBlcmZvcm0gTVgg bG9va3VwcyBidXQgbG9vayB1cCBkZXN0aW5hdGlvbiBkb21haW4gaW4gYW4KPiArQGNvZGV7PG9w ZW5zbXRwZC10YWJsZT59IGFuZCB1c2UgbWF0Y2hpbmcgcmVsYXkgdXJsIGFzIHJlbGF5IGhvc3Qu Cj4gKwo+ICtAaXRlbSBAY29kZXtob3N0fSAoZGVmYXVsdDogQGNvZGV7I2Z9KQo+ICsKPiArRG8g bm90IHBlcmZvcm0gTVggbG9va3VwcyBidXQgcmVsYXkgbWVzc2FnZXMgdG8gdGhlIHJlbGF5IGhv c3QKPiBkZXNjcmliZWQgYnkKPiArdGhlIHN0cmluZyByZWxheS11cmwuIFRoZSBmb3JtYXQgZm9y IHJlbGF5LXVybCBpcwo+ICtAc2FtcHtbcHJvdG86Ly9bbGFiZWxAQF1daG9zdFs6cG9ydF19LiBU aGUgZm9sbG93aW5nIHByb3RvY29scyBhcmUKPiBhdmFpbGFibGU6Cj4gKwo+ICtAbXVsdGl0YWJs ZSB7YWFhYWFhYWFhYX0KPiB7YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWF9Cj4gK0BpdGVtIHNtdHAKPiArQHRhYiBOb3JtYWwgU01U UCBzZXNzaW9uIHdpdGggb3Bwb3J0dW5pc3RpYyBTVEFSVFRMUyAodGhlIGRlZmF1bHQpLgo+ICtA aXRlbSBzbXRwK3Rscwo+ICtAdGFiIE5vcm1hbCBTTVRQIHNlc3Npb24gd2l0aCBtYW5kYXRvcnkg U1RBUlRUTFNALgo+ICtAaXRlbSBzbXRwK25vdGxzCj4gK0B0YWIgUGxhaW4gdGV4dCBTTVRQIHNl c3Npb24gd2l0aG91dCBUTFNALgo+ICtAaXRlbSBsbXRwCj4gK0B0YWIgTE1UUCBzZXNzaW9uLsKg IHBvcnQgaXMgcmVxdWlyZWQuCj4gK0BpdGVtIHNtdHBzCj4gK0B0YWIgU01UUCBzZXNzaW9uIHdp dGggZm9yY2VkIFRMUyBvbiBjb25uZWN0aW9uLCBkZWZhdWx0IHBvcnQgaXMKPiArQGl0ZW0KPiAr QHRhYiA0NjUuCj4gK0BlbmQgbXVsdGl0YWJsZQo+ICsKPiArVW5sZXNzIG5vdGVkLCBwb3J0IGRl ZmF1bHRzIHRvIDI1Lgo+ICsKPiArVGhlIGxhYmVsIGNvcnJlc3BvbmRzIHRvIGFuIGVudHJ5IGlu IGEgY3JlZGVudGlhbHMgdGFibGUsIGFzCj4gZG9jdW1lbnRlZCBpbgo+ICtAc2FtcHt0YWJsZSg1 KX0uIEl0IGlzIHVzZWQgd2l0aCB0aGUgQHNhbXB7InNtdHArdGxzIn0gYW5kCj4gQHNhbXB7InNt dHBzIn0gcHJvdG9jb2xzIGZvcgo+ICthdXRoZW50aWNhdGlvbi4gU2VydmVyIGNlcnRpZmljYXRl cyBmb3IgdGhvc2UgcHJvdG9jb2xzIGFyZSB2ZXJpZmllZAo+IGJ5Cj4gK2RlZmF1bHQuCj4gKwo+ ICtAaXRlbSBAY29kZXtwa2l9IChkZWZhdWx0OiBAY29kZXsjZn0pCj4gKwo+ICtGb3Igc2VjdXJl IGNvbm5lY3Rpb25zLCB1c2UgdGhlIGNlcnRpZmljYXRlIGFzc29jaWF0ZWQgd2l0aAo+ICtAY29k ZXs8b3BlbnNtdHBkLXBraT59IChkZWNsYXJlZCBpbiBhIHBraSBkaXJlY3RpdmUpIHRvIHByb3Zl IHRoZQo+ICtjbGllbnQncyBpZGVudGl0eSB0byB0aGUgcmVtb3RlIG1haWwgc2VydmVyLgo+ICsK PiArQGl0ZW0gQGNvZGV7c3JzfSAoZGVmYXVsdDogQGNvZGV7I2Z9KQo+ICsKPiArSWYgQGNvZGV7 I3R9LCB0aGVuIHdoZW4gcmVsYXlpbmcgYSBtYWlsIHJlc3VsdGluZyBmcm9tIGEgZm9yd2FyZCwK PiB1c2UgdGhlIFNlbmRlcgo+ICtSZXdyaXRpbmcgU2NoZW1lIHRvIHJld3JpdGUgc2VuZGVyIGFk ZHJlc3MuCj4gKwo+ICtAaXRlbSBAY29kZXt0bHN9IChkZWZhdWx0OiBAY29kZXsjZn0pIGJvb2xl YW4gb3Igc3RyaW5nIGBgbm8tCj4gdmVyaWZ5JycKPiArCj4gK1doZW4gQGNvZGV7I3R9LCBSZXF1 aXJlIFRMUyB0byBiZSB1c2VkIHdoZW4gcmVsYXlpbmcsIHVzaW5nCj4gbWFuZGF0b3J5IFNUQVJU VExTIGJ5Cj4gK2RlZmF1bHQuIFdoZW4gdXNlZCB3aXRoIGEgc21hcnRob3N0LCB0aGUgcHJvdG9j b2wgbXVzdCBub3QgYmUKPiArQHNhbXB7InNtdHArbm90bHM6Ly8ifS4gV2hlbiBzdHJpbmcgQGNv ZGV7Im5vLXZlcmlmeSJ9LCB0aGVuIGRvIG5vdAo+IHJlcXVpcmUgYSB2YWxpZAo+ICtjZXJ0aWZp Y2F0ZS4KPiArCj4gK0BpdGVtIEBjb2Rle2F1dGh9IChkZWZhdWx0OiBAY29kZXsjZn0pIEBjb2Rl ezxvcGVuc210cGQtdGFibGU+fQo+ICsKPiArVXNlIHRoZSBhbGlzdCBAY29kZXs8b3BlbnNtdHBk LXRhYmxlPn0gZm9yIGNvbm5lY3RpbmcgdG8gcmVsYXktdXJsCj4gK3VzaW5nIGNyZWRlbnRpYWxz LiBUaGlzIG9wdGlvbiBpcyB1c2FibGUgb25seSB3aXRoIGZpZWxkbmFtZQo+IEBjb2Rle2hvc3R9 IG9wdGlvbi4KPiArCj4gK0BpdGVtIEBjb2Rle21haWwtZnJvbX0gKGRlZmF1bHQ6IEBjb2RleyNm fSkgc3RyaW5nCj4gKwo+ICtVc2UgdGhlIHN0cmluZyBtYWlsYWRkcmVzcyBhcyBNQUlMIEZST00g YWRkcmVzcyB3aXRoaW4gdGhlIFNNVFAKPiB0cmFuc2FjdGlvbi4KPiArCj4gK0BpdGVtIEBjb2Rl e3NyY30gKGRlZmF1bHQ6IEBjb2RleyNmfSkgc3RyaW5nIHwgQGNvZGV7PG9wZW5zbXRwZC0KPiB0 YWJsZT59Cj4gKwo+ICtVc2UgdGhlIHN0cmluZyBvciBAY29kZXs8b3BlbnNtdHBkLXRhYmxlPn0g c291cmNlYWRkciBmb3IgdGhlCj4gK3NvdXJjZSBJUCBhZGRyZXNzLCB3aGljaCBpcyB1c2VmdWwg b24gbWFjaGluZXMgd2l0aCBtdWx0aXBsZQo+IGludGVyZmFjZXMuIElmCj4gK3RoZSBsaXN0IGNv bnRhaW5zIG1vcmUgdGhhbiBvbmUgYWRkcmVzcywgYWxsIG9mIHRoZW0gYXJlIHVzZWQgaW4KPiBz dWNoIGEgd2F5Cj4gK3RoYXQgdHJhZmZpYyBpcyByb3V0ZWQgYXMgZWZmaWNpZW50bHkgYXMgcG9z c2libGUuCj4gK0BlbmQgaXRlbWl6ZQo+ICsKPiArQGl0ZW0gRGF0YSBUeXBlOiBvcGVuc210cGQt ZmlsdGVyCj4gKwo+ICtUaGlzIGRhdGEgdHlwZSByZXByZXNlbnRzIHRoZSBjb25maWd1cmF0aW9u IG9mIGFuCj4gK0Bjb2RlezxvcGVuc210cGQtZmlsdGVyPn0uIFRoaXMgaXMgdGhlIGZpbHRlciBy ZWNvcmQgb25lIHNob3VsZCB1c2UKPiAraWYgdGhleSB3YW50IHRvIHVzZSBhbiBleHRlcm5hbCBw YWNrYWdlIHRvIGZpbHRlciBlbWFpbCBlZzogcnNwYW1kCj4gb3IKPiArc3BhbWFzc2Fzc2luLgo+ ICsKPiArQGl0ZW1pemUKPiArQGl0ZW0gQGNvZGV7bmFtZX0gKGRlZmF1bHQ6IEBjb2RleyNmfSkK PiArCj4gK1RoZSBzdHJpbmcgbmFtZSBvZiB0aGUgZmlsdGVyLgo+ICsKPiArQGl0ZW0gQGNvZGV7 cHJvY30gKGRlZmF1bHQ6IEBjb2RleyNmfSkKPiArCj4gK1RoZSBzdHJpbmcgY29tbWFuZCBvciBw cm9jZXNzIG5hbWUuwqAgSWYgQGNvZGV7cHJvYy1leGVjfSBpcwo+IEBjb2RleyN0fSwgQGNvZGV7 cHJvY30gaXMKPiArdHJlYXRlZCBhcyBhIGNvbW1hbmQgdG8gZXhlY3V0ZS7CoCBPdGhlcndpc2Us IGl0IGlzIGEgcHJvY2VzcyBuYW1lLgo+ICsKPiArQGl0ZW0gQGNvZGV7cHJvYy1leGVjfSAoZGVm YXVsdDogQGNvZGV7I2Z9KQo+ICtAZW5kIGl0ZW1pemUKPiArCj4gK0BpdGVtIERhdGEgVHlwZTog b3BlbnNtdHBkLWZpbHRlci1waGFzZQo+ICsKPiArVGhpcyBkYXRhIHR5cGUgcmVwcmVzZW50cyB0 aGUgY29uZmlndXJhdGlvbiBvZiBhbgo+ICtAY29kZXs8b3BlbnNtdHBkLWZpbHRlci1waGFzZT59 Lgo+ICsKPiArSW4gYSByZWd1bGFyIHdvcmtmbG93LCBzbXRwZCg4KSBtYXkgYWNjZXB0IG9yIHJl amVjdCBhIG1lc3NhZ2UgYmFzZWQKPiBvbmx5IG9uCj4gK3RoZSBjb250ZW50IG9mIGVudmVsb3Bl cy4gSXRzIGRlY2lzaW9ucyBhcmUgYWJvdXQgdGhlIGhhbmRsaW5nIG9mCj4gdGhlIG1lc3NhZ2Us Cj4gK25vdCBhYm91dCB0aGUgaGFuZGxpbmcgb2YgYW4gYWN0aXZlIHNlc3Npb24uCj4gKwo+ICtG aWx0ZXJpbmcgZXh0ZW5kcyB0aGUgZGVjaXNpb24gbWFraW5nIHByb2Nlc3MgYnkgYWxsb3dpbmcg c210cGQoOCkKPiB0byBzdG9wIGF0Cj4gK2VhY2ggcGhhc2Ugb2YgYW4gU01UUCBzZXNzaW9uLCBj aGVjayB0aGF0IG9wdGlvbnMgYXJlIG1ldCwgdGhlbgo+IGRlY2lkZSBpZiBhCj4gK3Nlc3Npb24g aXMgYWxsb3dlZCB0byBtb3ZlIGZvcndhcmQuCj4gKwo+ICtXaXRoIGZpbHRlcmluZyB2aWEgYW4g QGNvZGV7PG9wZW5zbXRwZC1maWx0ZXItcGhhc2U+fSByZWNvcmQsIGEKPiArc2Vzc2lvbiBtYXkg YmUgaW50ZXJydXB0ZWQgYXQgYW55IHBoYXNlIGJlZm9yZSBhbiBlbnZlbG9wZSBpcwo+IGNvbXBs ZXRlLiBBCj4gK21lc3NhZ2UgbWF5IGFsc28gYmUgcmVqZWN0ZWQgYWZ0ZXIgYmVpbmcgc3VibWl0 dGVkLCByZWdhcmRsZXNzIG9mCj4gd2hldGhlciB0aGUKPiArZW52ZWxvcGUgd2FzIGFjY2VwdGVk IG9yIG5vdC4KPiArCj4gK0BpdGVtaXplCj4gK0BpdGVtIEBjb2Rle25hbWV9IChkZWZhdWx0OiBA Y29kZXsjZn0pCj4gKwo+ICtUaGUgc3RyaW5nIG5hbWUgb2YgdGhlIGZpbHRlciBwaGFzZS4KPiAr Cj4gK0BpdGVtIEBjb2Rle3BoYXNlLW5hbWV9IChkZWZhdWx0OiBAY29kZXsjZn0pCj4gKwo+ICtU aGUgc3RyaW5nIG5hbWUgb2YgdGhlIHBoYXNlLiBWYWxpZCB2YWx1ZXMgYXJlOgo+ICsKPiArQG11 bHRpdGFibGUge2FhYWFhYWFhYWFhfQo+IHthYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh YWFhYWFhYWFhYWFhYWF9Cj4gK0BpdGVtIGBgY29ubmVjdCcnCj4gK0B0YWIgdXBvbiBjb25uZWN0 aW9uLCBiZWZvcmUgYSBiYW5uZXIgaXMgZGlzcGxheWVkCj4gK0BpdGVtIGBgaGVsbycnCj4gK0B0 YWIgYWZ0ZXIgSEVMTyBjb21tYW5kIGlzIHN1Ym1pdHRlZAo+ICtAaXRlbSBgYGVobG8nJwo+ICtA dGFiIGFmdGVyIEVITE8gY29tbWFuZCBpcyBzdWJtaXR0ZWQKPiArQGl0ZW0gYGBtYWlsLWZyb20n Jwo+ICtAdGFiIGFmdGVyIE1BSUwgRlJPTSBjb21tYW5kIGlzIHN1Ym1pdHRlZAo+ICtAaXRlbSBg YHJjcHQtdG8nJwo+ICtAdGFiIGFmdGVyIFJDUFQgVE8gY29tbWFuZCBpcyBzdWJtaXR0ZWQKPiAr QGl0ZW0gYGBkYXRhJycKPiArQHRhYiBhZnRlciBEQVRBIGNvbW1hbmQgaXMgc3VibWl0dGVkCj4g K0BpdGVtIGBgY29tbWl0JycKPiArQHRhYiBhZnRlciBtZXNzYWdlIGlzIGZ1bGx5IGlzIHN1Ym1p dHRlZAo+ICtAZW5kIG11bHRpdGFibGUKPiArCj4gK0BpdGVtIEBjb2Rle29wdGlvbnN9IChkZWZh dWx0IEBjb2RleyNmfSkKPiArCj4gK0EgbGlzdCBvZiB1bmlxdWUgQGNvZGV7PG9wZW5zbXRwZC1v cHRpb24+fSByZWNvcmRzLgo+ICsKPiArQXQgZWFjaCBwaGFzZSwgdmFyaW91cyBvcHRpb25zLCBz cGVjaWZpZWQgYnkgYSBsaXN0IG9mCj4gK0Bjb2RlezxvcGVuc210cGQtb3B0aW9uPn0sIG1heSBi ZSBjaGVja2VkLiBUaGUKPiArQGNvZGV7PG9wZW5zbXRwZC1vcHRpb24+fSdzIGZpZWxkbmFtZSAn b3B0aW9uJyB2YWx1ZXMgb2Y6Cj4gYGBmY3JkbnMnJywKPiArYGByZG5zJycsIGFuZCBgYHNyYycn IGRhdGEgYXJlIGF2YWlsYWJsZSBpbiBhbGwgcGhhc2VzLCBidXQgb3RoZXIKPiBkYXRhIG11c3Qg aGF2ZQo+ICtiZWVuIGFscmVhZHkgc3VibWl0dGVkIGJlZm9yZSB0aGV5IGFyZSBhdmFpbGFibGUu IE9wdGlvbnMgd2l0aCBhCj4gQHNhbXB7PHRhYmxlPn0KPiArbmV4dCB0byB0aGVtIHJlcXVpcmUg dGhlIEBjb2RlezxvcGVuc210cGQtb3B0aW9uPn0ncyBmaWVsZG5hbWUKPiArQGNvZGV7ZGF0YX0g dG8gYmUgYW4gQGNvZGV7PG9wZW5zbXRwZC10YWJsZT59LiBUaGVyZSBhcmUgdGhlCj4gYXZhaWxh YmxlCj4gK29wdGlvbnM6Cj4gKwo+ICtAbXVsdGl0YWJsZSB7YWFhYWFhYWFhYWFhYWFhYWF9Cj4g e2FhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhfQo+ICtAaXRlbSBmY3JkbnMK PiArQHRhYiBmb3J3YXJkLWNvbmZpcm1lZCByZXZlcnNlIEROUyBpcyB2YWxpZAo+ICtAaXRlbSBy ZG5zCj4gK0B0YWIgc2Vzc2lvbiBoYXMgYSByZXZlcnNlIEROUwo+ICtAaXRlbSByZG5zIDx0YWJs ZT4KPiArQHRhYiBzZXNzaW9uIGhhcyBhIHJldmVyc2UgRE5TIGluIHRhYmxlCj4gK0BpdGVtIHNy YyA8dGFibGU+Cj4gK0B0YWIgc291cmNlIGFkZHJlc3MgaXMgaW4gdGFibGUKPiArQGl0ZW0gaGVs byA8dGFibGU+Cj4gK0B0YWIgaGVsbyBuYW1lIGlzIGluIHRhYmxlCj4gK0BpdGVtIGF1dGgKPiAr QHRhYiBzZXNzaW9uIGlzIGF1dGhlbnRpY2F0ZWQKPiArQGl0ZW0gYXV0aCA8dGFibGU+Cj4gK0B0 YWIgc2Vzc2lvbiB1c2VybmFtZSBpcyBpbiB0YWJsZQo+ICtAaXRlbSBtYWlsLWZyb20gPHRhYmxl Pgo+ICtAdGFiIHNlbmRlciBhZGRyZXNzIGlzIGluIHRhYmxlCj4gK0BpdGVtIHJjcHQtdG8gPHRh YmxlPgo+ICtAdGFiIHJlY2lwaWVudCBhZGRyZXNzIGlzIGluIHRhYmxlCj4gK0BlbmQgbXVsdGl0 YWJsZQo+ICsKPiArVGhlc2UgY29uZGl0aW9ucyBtYXkgYWxsIGJlIG5lZ2F0ZWQgYnkgc2V0dGlu Zwo+ICtAY29kZXs8b3BlbnNtdHBkLW9wdGlvbj59J3MgZmllbGRuYW1lIEBjb2Rle25vdH0gdG8g QGNvZGV7I3R9Lgo+ICsKPiArQW55IGNvbmRpdGlvbnMgdGhhdCByZXF1aXJlIGEgdGFibGUgbWF5 IGluZGljYXRlIHRoYXQgdGFibGVzIGluY2x1ZGUKPiByZWdleHMKPiArc2V0dGluZyBAY29kZXs8 b3BlbnNtdHBkLW9wdGlvbj59J3MgZmllbGRuYW1lIEBjb2Rle3JlZ2V4fSB0bwo+IEBjb2RleyN0 fS4KPiArCj4gK0BpdGVtIEBjb2Rle2RlY2lzaW9ufQo+ICsKPiArQSBzdHJpbmcgZGVjaXNpb24g dG8gYmUgdGFrZW4uIFNvbWUgZGVjaXNpb25zIHJlcXVpcmUgYW4KPiBAY29kZXttZXNzYWdlfSBv cgo+ICtAY29kZXt2YWx1ZX0uIFZhbGlkIHN0cmluZ3MgYXJlOgo+ICsKPiArQG11bHRpdGFibGUg e2FhYWFhYWFhYWFhYWFhYWFhYWFhfQo+IHthYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh YWFhYWFhYWFhYWFhYWFhfQo+ICtAaXRlbSBgYGJ5cGFzcycnCj4gK0B0YWIgdGhlIHNlc3Npb24g b3IgdHJhbnNhY3Rpb24gYnlwYXNzZXMgZmlsdGVycwo+ICtAaXRlbSBgYGRpc2Nvbm5lY3QnJyBt ZXNzYWdlCj4gK0B0YWIgdGhlIHNlc3Npb24gaXMgZGlzY29ubmVjdGVkIHdpdGggbWVzc2FnZQo+ ICtAaXRlbSBgYGp1bmsnJwo+ICtAdGFiIHRoZSBzZXNzaW9uIG9yIHRyYW5zYWN0aW9uIGlzIGp1 bmtlZCwgaS5lLiwgYW4KPiArQGl0ZW0KPiArQHRhYiDigJhYLVNwYW06IHllc+KAmSBoZWFkZXIg aXMgYWRkZWQgdG8gYW55IG1lc3NhZ2VzCj4gK0BpdGVtIGBgcmVqZWN0JycgbWVzc2FnZQo+ICtA dGFiIHRoZSBjb21tYW5kIGlzIHJlamVjdGVkIHdpdGggbWVzc2FnZQo+ICtAaXRlbSBgYHJld3Jp dGUnJyB2YWx1ZQo+ICtAdGFiIHRoZSBjb21tYW5kIHBhcmFtZXRlciBpcyByZXdyaXR0ZW4gd2l0 aCB2YWx1ZQo+ICtAZW5kIG11bHRpdGFibGUKPiArCj4gK0RlY2lzaW9ucyB0aGF0IGludm9sdmUg YSBtZXNzYWdlIHJlcXVpcmUgdGhhdCB0aGUgbWVzc2FnZSBiZSBSRkMKPiB2YWxpZCwKPiArbWVh bmluZyB0aGF0IHRoZXkgc2hvdWxkIGVpdGhlciBzdGFydCB3aXRoIGEgNHh4IG9yIDV4eCBzdGF0 dXMgY29kZS4KPiArRGVzY2lzaW9ucyBjYW4gYmUgdGFrZW4gYXQgYW55IHBoYXNlLCB0aG91Z2gg anVua2luZyBjYW4gb25seSBoYXBwZW4KPiBiZWZvcmUKPiArYSBtZXNzYWdlIGlzIGNvbW1pdHRl ZC4KPiArCj4gK0BpdGVtIEBjb2Rle21lc3NhZ2V9IChkZWZhdWx0IEBjb2RleyNmfSkKPiArCj4g K0Egc3RyaW5nIG1lc3NhZ2UgYmVnaW5uaW5nIHdpdGggYSA0eHggb3IgNXh4IHN0YXR1cyBjb2Rl Lgo+ICsKPiArQGl0ZW0gQGNvZGV7dmFsdWV9IChkZWZhdWx0OiBAY29kZXsjZn0pCj4gKwo+ICtB IG51bWJlciB2YWx1ZS7CoCBAY29kZXt2YWx1ZX0gYW5kIEBjb2Rle21lc3NhZ2V9IGFyZSBtdXR1 YWxseQo+IGV4Y2x1c2l2ZS4KPiArQGVuZCBpdGVtaXplCj4gKwo+ICtAaXRlbSBEYXRhIFR5cGU6 IG9wZW5zbXRwZC1vcHRpb24KPiArCj4gK1RoaXMgZGF0YSB0eXBlIHJlcHJlc2VudHMgdGhlIGNv bmZpZ3VyYXRpb24gb2YgYW4KPiArQGNvZGV7PG9wZW5zbXRwZC1vcHRpb24+fSwgd2hpY2ggaXMg dXNlZCBieQo+ICtAY29kZXs8b3BlbnNtdHBkLWZpbHRlci1waGFzZT59IGFuZCBAY29kZXs8b3Bl bnNtdHBkLW1hdGNoPn0KPiArdG8gbWF0Y2ggdmFyaW91cyBvcHRpb25zIGZvciBlbWFpbC4KPiAr Cj4gK0BpdGVtaXplCj4gK0BpdGVtIEBjb2Rle2NvbmRpdGl0aW9ufSAoZGVmYXVsdCBAY29kZXsj Zn0pCj4gKwo+ICtBIHN0cmluZyBvcHRpb24gdG8gYmUgdGFrZW4uIFNvbWUgb3B0aW9ucyByZXF1 aXJlIGEgc3RyaW5nIG9yIGFuCj4gK0Bjb2RlezxvcGVuc210cGQtdGFibGU+fSB2aWEgdGhlIGZp ZWxkbmFtZSBkYXRhLiBXaGVuIHRoZSBvcHRpb24KPiArcmVjb3JkIGlzIHVzZWQgaW5zaWRlIG9m IGFuIEBjb2RlezxvcGVuc210cGQtZmlsdGVyLXBoYXNlPn0sIHRoZW4KPiArdmFsaWQgc3RyaW5n cyBhcmU6Cj4gKwo+ICtBdCBlYWNoIHBoYXNlLCB2YXJpb3VzIG9wdGlvbnMgbWF5IGJlIG1hdGNo ZWQuIFRoZSBmY3JkbnMsIHJkbnMsIGFuZAo+IHNyYwo+ICtkYXRhIGFyZSBhdmFpbGFibGUgaW4g YWxsIHBoYXNlcywgYnV0IG90aGVyIGRhdGEgbXVzdCBoYXZlIGJlZW4KPiBhbHJlYWR5Cj4gK3N1 Ym1pdHRlZCBiZWZvcmUgdGhleSBhcmUgYXZhaWxhYmxlLgo+ICsKPiArQG11bHRpdGFibGUge2Fh YWFhYWFhYWFhYWFhYWFhYWF9Cj4ge2FhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh YWFhfQo+ICtAaXRlbSBgYGZjcmRucycnCj4gK0B0YWIgZm9yd2FyZC1jb25maXJtZWQgcmV2ZXJz ZSBETlMgaXMgdmFsaWQKPiArQGl0ZW0gYGByZG5zJycKPiArQHRhYiBzZXNzaW9uIGhhcyBhIHJl dmVyc2UgRE5TCj4gK0BpdGVtIGBgcmRucycnIDx0YWJsZT4KPiArQHRhYiBzZXNzaW9uIGhhcyBh IHJldmVyc2UgRE5TIGluIHRhYmxlCj4gK0BpdGVtIGBgc3JjJycgPHRhYmxlPgo+ICtAdGFiIHNv dXJjZSBhZGRyZXNzIGlzIGluIHRhYmxlCj4gK0BpdGVtIGBgaGVsbycnIDx0YWJsZT4KPiArQHRh YiBoZWxvIG5hbWUgaXMgaW4gdGFibGUKPiArQGl0ZW0gYGBhdXRoJycKPiArQHRhYiBzZXNzaW9u IGlzIGF1dGhlbnRpY2F0ZWQKPiArQGl0ZW0gYGBhdXRoJycgPHRhYmxlPgo+ICtAdGFiIHNlc3Np b24gdXNlcm5hbWUgaXMgaW4gdGFibGUKPiArQGl0ZW0gYGBtYWlsLWZyb20nJyA8dGFibGU+Cj4g K0B0YWIgc2VuZGVyIGFkZHJlc3MgaXMgaW4gdGFibGUKPiArQGl0ZW0gYGByY3B0LXRvJycgPHRh YmxlPgo+ICtAdGFiIHJlY2lwaWVudCBhZGRyZXNzIGlzIGluIHRhYmxlCj4gK0BlbmQgbXVsdGl0 YWJsZQo+ICsKPiArV2hlbiBAY29kZXs8b3BlbnNtdHBkLW9wdGlvbj59IGlzIHVzZWQgaW5zaWRl IG9mIGFuCj4gK0Bjb2RlezxvcGVuc210cGQtbWF0Y2g+fSwgdGhlbiB2YWxpZCBzdHJpZ3MgZm9y IGZpZWxkbmFtZQo+IEBjb2Rle29wdGlvbn0KPiArYXJlOiBgYGZvcicnLCBgYGZvciBhbnknJywg YGBmb3IgbG9jYWwnJywgYGBmb3IgZG9tYWluJycsIGBgZm9yCj4gcmNwdC10bycnLCBgYGZyb20g YW55JycKPiArYGBmcm9tIGF1dGgnJywgYGBmcm9tIGxvY2FsJycsIGBgZnJvbSBtYWlsLWZyb20n JywgYGBmcm9tIHJkbnMnJywKPiBgYGZyb20gc29ja2V0JycsCj4gK2BgZnJvbSBzcmMnJywgYGBh dXRoJycsIGBgaGVsbycnLCBgYG1haWwtZnJvbScnLCBgYHJjcHQtdG8nJywKPiBgYHRhZycnLCBv ciBgYHRscycnLgo+ICsKPiArQGl0ZW0gQGNvZGV7ZGF0YX0gKGRlZmF1bHQgQGNvZGV7I2Z9KSBA Y29kZXs8b3BlbnNtdHBkLXRhYmxlPn0KPiArCj4gK1NvbWUgb3B0aW9ucyByZXF1aXJlIGEgdGFi bGUgdG8gYmUgcHJlc2VudC4gT25lIHdvdWxkIHNwZWNpZnkgdGhhdAo+IHRhYmxlCj4gK2hlcmUu Cj4gK0BpdGVtIEBjb2Rle3JlZ2V4fSAoZGVmYXVsdDogQGNvZGV7I2Z9KSBib29sZWFuCj4gKwo+ ICtBbnkgb3B0aW9ucyB1c2luZyBhIHRhYmxlIG1heSBpbmRpY2F0ZSB0aGF0IHRhYmxlcyBob2xk IHJlZ2V4IGJ5Cj4gK3ByZWZpeGluZyB0aGUgdGFibGUgbmFtZSB3aXRoIHRoZSBrZXl3b3JkIHJl Z2V4Lgo+ICsKPiArQGl0ZW0gQGNvZGV7bm90fSAoZGVmYXVsdDogQGNvZGV7I2Z9KSBib29sZWFu Cj4gKwo+ICtXaGVuIEBjb2RleyN0fSwgdGhpcyBvcHRpb24gcmVjb3JkIGlzIG5lZ2F0ZWQuCj4g K0BlbmQgaXRlbWl6ZQo+ICsKPiArQGl0ZW0gRGF0YSBUeXBlOiBvcGVuc210cGQtdGFibGUKPiAr Cj4gK1RoaXMgZGF0YSB0eXBlIHJlcHJlc2VudHMgdGhlIGNvbmZpZ3VyYXRpb24gb2YgYW4KPiAr QGNvZGV7PG9wZW5zbXRwZC10YWJsZT59Lgo+ICsKPiArQGl0ZW1pemUKPiArQGl0ZW0gQGNvZGV7 bmFtZX0gKGRlZmF1bHQgQGNvZGV7I2Z9KQo+ICsKPiArQGNvZGV7bmFtZX0gaXMgdGhlIG5hbWUg b2YgdGhlIEBjb2RlezxvcGVuc210cGQtdGFibGU+fSByZWNvcmQuCj4gKwo+ICtAaXRlbSBAY29k ZXtkYXRhfSAoZGVmYXVsdDogQGNvZGV7I2Z9KQo+ICsKPiArQGNvZGV7ZGF0YX0gZXhwZWN0cyBh IGxpc3Qgb2Ygc3RyaW5ncyBvciBhbiBhbGlzdCwgd2hpY2ggaXMgYSBsaXN0Cj4gb2YKPiArY29u cyBjZWxscy7CoCBlZzogQGNvZGV7KGRhdGEgKGxpc3QgKCJqYW1lcyIgLiAicGFzc3dvcmQiKSkp fSBPUgo+ICtAY29kZXsoZGF0YSAobGlzdCAoImdudS5vcmciICJmc2Yub3JnIikpKX0uCj4gK0Bl bmQgaXRlbWl6ZQo+ICsKPiArQGl0ZW0gRGF0YSBUeXBlOiBvcGVuc210cGQtcGtpCj4gKwo+ICtU aGlzIGRhdGEgdHlwZSByZXByZXNlbnRzIHRoZSBjb25maWd1cmF0aW9uIG9mIGFuCj4gK0Bjb2Rl ezxvcGVuc210cGQtcGtpPn0uCj4gKwo+ICtAaXRlbWl6ZQo+ICtAaXRlbSBAY29kZXtkb21haW59 IChkZWZhdWx0IEBjb2RleyNmfSkKPiArCj4gK0Bjb2Rle2RvbWFpbn0gaXMgdGhlIHN0cmluZyBu YW1lIG9mIHRoZSBAY29kZXs8b3BlbnNtdHBkLXBraT59Cj4gcmVjb3JkLgo+ICsKPiArQGl0ZW0g QGNvZGV7Y2VydH0gKGRlZmF1bHQ6IEBjb2RleyNmfSkKPiArCj4gK0Bjb2Rle2NlcnR9IChkZWZh dWx0OiBAY29kZXsjZn0pCj4gKwo+ICtAY29kZXtjZXJ0fSBpcyB0aGUgc3RyaW5nIGNlcnRpZmlj YXRlIGZpbGVuYW1lIHRvIHVzZSBmb3IgdGhpcyBwa2kuCj4gKwo+ICtAaXRlbSBAY29kZXtrZXl9 IChkZWZhdWx0OiBAY29kZXsjZn0pCj4gKwo+ICtAY29kZXtrZXl9IGlzIHRoZSBzdHJpbmcgY2Vy dGlmaWNhdGUgZmFsZW5hbWUgdG8gdXNlIGZvciB0aGlzIHBraS4KPiArCj4gK0BpdGVtIEBjb2Rl e2RoZX0gKGRlZmF1bHQ6IEBjb2RleyJub25lIn0pCj4gKwo+ICtTcGVjaWZ5IHRoZSBESEUgc3Ry aW5nIHBhcmFtZXRlciB0byB1c2UgZm9yIERIRSBjaXBoZXIgc3VpdGVzIHdpdGgKPiBob3N0Cj4g K3BraW5hbWUuIFZhbGlkIHBhcmFtZXRlciB2YWx1ZXMgYXJlIGBgbm9uZScnLCBgYGxlZ2FjeScn LCBvcgo+IGBgYXV0bycnLiBGb3IgYGBsZWdhY3knJywgYQo+ICtmaXhlZCBrZXkgbGVuZ3RoIG9m IDEwMjQgYml0cyBpcyB1c2VkLCB3aGVyZWFzIGZvciBgYGF1dG8nJywgdGhlIGtleQo+IGxlbmd0 aCBpcwo+ICtkZXRlcm1pbmVkIGF1dG9tYXRpY2FsbHkuIFRoZSBkZWZhdWx0IGlzIGBgbm9uZScn LCB3aGljaCBkaXNhYmxlcwo+IERIRSBjaXBoZXIKPiArc3VpdGVzLgo+ICtAZW5kIGl0ZW1pemUK PiArCj4gK0BpdGVtIERhdGEgVHlwZTogb3BlbnNtdHBkLW1haWxkaXIKPiArCj4gK0BpdGVtaXpl Cj4gK0BpdGVtIEBjb2Rle3BhdGhuYW1lfSAoZGVmYXVsdDogQGNvZGV7In4vTWFpbGRpciJ9KQo+ ICsKPiArRGVsaXZlciB0aGUgbWVzc2FnZSB0byB0aGUgbWFpbGRpciBpZiBwYXRobmFtZSBpZiBz cGVjaWZpZWQsIG9yIGJ5Cj4gZGVmYXVsdAo+ICt0byBAc2FtcHt+L01haWxkaXJ9Lgo+ICsKPiAr VGhlIHBhdGhuYW1lIG1heSBjb250YWluIGZvcm1hdCBzcGVjaWZpZXJzIHRoYXQgYXJlIGV4cGFu ZGVkIGJlZm9yZQo+IHVzZQo+ICsoc2VlIEZPUk1BVCBTUEVDSUZJRVJTKS4KPiArCj4gK0BpdGVt IEBjb2Rle2p1bmt9IChkZWZhdWx0OiBAY29kZXsjZn0pCj4gKwo+ICtJZiB0aGUganVuayBhcmd1 bWVudCBpcyBAY29kZXsjdH0sIHRoZW4gdGhlIG1lc3NhZ2Ugd2lsbCBiZSBtb3ZlZCB0bwo+IHRo ZSBAc2FtcHvigJhKdW5r4oCZfQo+ICtmb2xkZXIgaWYgaXQgY29udGFpbnMgYSBwb3NpdGl2ZSBA c2FtcHvigJhYLVNwYW3igJl9IGhlYWRlci4gVGhpcyBmb2xkZXIKPiB3aWxsIGJlCj4gK2NyZWF0 ZWQgdW5kZXIgcGF0aG5hbWUgaWYgaXQgZG9lcyBub3QgeWV0IGV4aXN0Lgo+ICtAZW5kIGl0ZW1p emUKPiArCj4gK0BpdGVtIERhdGEgVHlwZTogb3BlbnNtdHBkLW1kYQo+ICsKPiArQGl0ZW1pemUK PiArQGl0ZW0gQGNvZGV7bmFtZX0KPiArCj4gK1RoZSBzdHJpbmcgbmFtZSBmb3IgdGhpcyBNREEg Y29tbWFuZC4KPiArCj4gK0BpdGVtIEBjb2Rle2NvbW1hbmR9Cj4gKwo+ICtEZWxlZ2F0ZSB0aGUg ZGVsaXZlcnkgdG8gYSBjb21tYW5kIHRoYXQgcmVjZWl2ZXMgdGhlIG1lc3NhZ2Ugb24gaXRzCj4g c3RhbmRhcmQKPiAraW5wdXQuCj4gKwo+ICtUaGUgY29tbWFuZCBtYXkgY29udGFpbiBmb3JtYXQg c3BlY2lmaWVycyB0aGF0IGFyZSBleHBhbmRlZCBiZWZvcmUKPiB1c2UgKHNlZQo+ICtGT1JNQVQg U1BFQ0lGSUVSUykuCj4gK0BlbmQgaXRlbWl6ZQo+ICsKPiArQGl0ZW0gRGF0YSBUeXBlOiBvcGVu c210cGQtcXVldWUKPiArCj4gK0BpdGVtaXplCj4gK0BpdGVtIEBjb2Rle2NvbXByZXNzaW9ufSAo ZGVmYXVsdCBAY29kZXsjZn0pCj4gKwo+ICtTdG9yZSBxdWV1ZSBmaWxlcyBpbiBhIGNvbXByZXNz ZWQgZm9ybWF0LiBUaGlzIG1heSBiZSB1c2VmdWwgdG8gc2F2ZQo+IGRpc2sKPiArc3BhY2UuCj4g Kwo+ICtAaXRlbSBAY29kZXtlbmNyeXB0aW9ufSAoZGVmYXVsdCBAY29kZXsjZn0pCj4gKwo+ICtF bmNyeXB0IHF1ZXVlIGZpbGVzIHdpdGggRVZQQG1hdGh7X2Flc31AbWF0aHtfMjU2fUBtYXRoe19n Y219KDMpLiBJZgo+IG5vIGtleSBpcyBzcGVjaWZpZWQsIGl0IGlzCj4gK3JlYWQgd2l0aCBnZXRw YXNzKDMpLiBJZiB0aGUgc3RyaW5nIHN0ZGluIG9yIGEgc2luZ2xlIGRhc2ggKOKAmC3igJkpIGlz Cj4gZ2l2ZW4KPiAraW5zdGVhZCBvZiBhIGtleSwgdGhlIGtleSBpcyByZWFkIGZyb20gdGhlIHN0 YW5kYXJkIGlucHV0Lgo+ICsKPiArQGl0ZW0gQGNvZGV7dHRsLWRlbGF5fSAoZGVmYXVsdCBAY29k ZXsjZn0pCj4gKwo+ICtTZXQgdGhlIGRlZmF1bHQgZXhwaXJhdGlvbiB0aW1lIGZvciB0ZW1wb3Jh cmlseSB1bmRlbGl2ZXJhYmxlCj4gbWVzc2FnZXMsCj4gK2dpdmVuIGFzIGEgcG9zaXRpdmUgZGVj aW1hbCBpbnRlZ2VyIGZvbGxvd2VkIGJ5IGEgdW5pdCBzLCBtLCBoLCBvcgo+IGQuIFRoZQo+ICtk ZWZhdWx0IGlzIGZvdXIgZGF5cyAoYGA0ZCcnKS4KPiArQGVuZCBpdGVtaXplCj4gKwo+ICtAaXRl bSBEYXRhIFR5cGU6IG9wZW5zbXRwZC1zbXRwCj4gKwo+ICtEYXRhIHR5cGUgcmVwcmVzZW50aW5n IGFuIEBjb2RlezxvcGVuc210cGQtc210cD59IHJlY29yZC4KPiArCj4gK0BpdGVtaXplCj4gK0Bp dGVtIEBjb2Rle2NpcGhlcnN9IChkZWZhdWx0OiBAY29kZXsjZn0pCj4gKwo+ICtTZXQgdGhlIGNv bnRyb2wgc3RyaW5nIGZvcgo+IFNTTEBtYXRoe19DVFh9QG1hdGh7X3NldH1AbWF0aHtfY2lwaGVy fUBtYXRoe19saXN0fSgzKS7CoCBUaGUgZGVmYXVsdAo+IGlzCj4gK8KgwqDCoMKgwqDCoMKgwqAg YGBISUdIOiFhTlVMTDohTUQ1JycuCj4gKwo+ICtAaXRlbSBAY29kZXtsaW1pdC1tYXgtbWFpbHN9 IChkZWZhdWx0OiBAY29kZXsxMDB9KQo+ICsKPiArTGltaXQgdGhlIG51bWJlciBvZiBtZXNzYWdl cyB0byBjb3VudCBmb3IgZWFjaCBzZXNzaW8KPiArCj4gK0BpdGVtIEBjb2Rle2xpbWl0LW1heC1y Y3B0fSAoZGVmYXVsdDogQGNvZGV7MTAwMH0pCj4gKwo+ICtMaW1pdCB0aGUgbnVtYmVyIG9mIHJl Y2lwaWVudHMgdG8gY291bnQgZm9yIGVhY2ggdHJhbnNhY3Rpb24uCj4gKwo+ICtAaXRlbSBAY29k ZXttYXgtbWVzc2FnZS1zaXplfSAoZGVmYXVsdDogQGNvZGV7MzVNfSkKPiArCj4gK1JlamVjdCBt ZXNzYWdlcyBsYXJnZXIgdGhhbiBzaXplLCBnaXZlbiBhcyBhIHBvc2l0aXZlIG51bWJlciBvZgo+ IGJ5dGVzIG9yIGFzCj4gK2Egc3RyaW5nIHRvIGJlIHBhcnNlZCB3aXRoIHNjYW5AbWF0aHtfc2Nh bGVkfSgzKS4KPiArCj4gK0BpdGVtIEBjb2Rle3N1Yi1hZGRyLWRlbGltIGNoYXJhY3Rlcn0gKGRl ZmF1bHQ6IEBjb2Rleyt9KQo+ICsKPiArV2hlbiByZXNvbHZpbmcgdGhlIGxvY2FsIHBhcnQgb2Yg YSBsb2NhbCBlbWFpbCBhZGRyZXNzLCBpZ25vcmUgdGhlCj4gQVNDSUkKPiArY2hhcmFjdGVyIGFu ZCBhbGwgY2hhcmFjdGVycyBmb2xsb3dpbmcgaXQuIFRoaXMgaXMgaGVscGZ1bCBmb3IgZW1haWwK PiArZmlsdGVycy4gQHNhbXB7ImFkbWluK2JpbGxzQEBnbnUub3JnIn0gaXMgdGhlIHNhbWUgZW1h aWwgYWRkcmVzcyBhcwo+ICtAc2FtcHsiYWRtaW5AQGdudS5vcmcifS4gQlVUIGFuIGVtYWlsIGZp bHRlciBjYW4gZmlsdGVyIGVtYWlscwo+IGFkZHJlc3NlZCB0byBmaXJzdAo+ICtlbWFpbCBhZGRy ZXNzIGludG8gYSAnQmlsbHMnIGVtYWlsIGZvbGRlci4KPiArQGVuZCBpdGVtaXplCj4gKwo+ICtA aXRlbSBEYXRhIFR5cGU6IG9wZW5zbXRwZC1zcnMKPiArCj4gK0BpdGVtaXplCj4gK0BpdGVtIEBj b2Rle2tleX0gKGRlZmF1bHQ6IEBjb2RleyNmfSkKPiArCj4gK1NldCB0aGUgc2VjcmV0IGtleSB0 byB1c2UgZm9yIFNSUywgdGhlIFNlbmRlciBSZXdyaXRpbmcgU2NoZW1lLgo+ICsKPiArQGl0ZW0g QGNvZGV7YmFja3VwLWtleX0gKGRlZmF1bHQ6IEBjb2RleyNmfSkKPiArCj4gK1NldCBhIGJhY2t1 cCBzZWNyZXQga2V5IHRvIHVzZSBhcyBhIGZhbGxiYWNrIGZvciBTUlNALiBUaGlzIGNhbiBiZQo+ IHVzZWQgdG8KPiAraW1wbGVtZW50IFNSUyBrZXkgcm90YXRpb24uCj4gKwo+ICtAaXRlbSBAY29k ZXt0dGwtZGVsYXl9IChkZWZhdWx0OiBAY29kZXsiNGQifSkKPiArCj4gK1NldCB0aGUgdGltZS10 by1saXZlIGRlbGF5IGZvciBTUlMgZW52ZWxvcGVzLiBBZnRlciB0aGlzIGRlbGF5LCBhCj4gYm91 bmNlCj4gK3JlcGx5IHRvIHRoZSBTUlMgYWRkcmVzcyB3aWxsIGJlIGRpc2NhcmRlZCB0byBsaW1p dCByaXNrcyBvZiBmb3JnZWQKPiArYWRkcmVzc2VzLgo+ICtAZW5kIGl0ZW1pemUKPiArCj4gK0Bp dGVtIEZvcm1hdCBTcGVjaWZpZXJzCj4gKwo+ICtTb21lIGNvbmZpZ3VyYXRpb24gcmVjb3JkcyBz dXBwb3J0IGV4cGFuc2lvbiBvZiB0aGVpciBwYXJhbWV0ZXJzIGF0Cj4gK3J1bnRpbWUuIFN1Y2gg cmVjb3JkcyAoZm9yIGV4YW1wbGUKPiArQGNvZGV7PG9wZW5zbXRwZC1tYWlsZGlyPn0sIEBjb2Rl ezxvcGVuc210cGQtbWRhPn0pIG1heSB1c2UKPiArZm9ybWF0IHNwZWNpZmllcnMgd2hpY2ggYXJl IGV4cGFuZGVkIGJlZm9yZSBkZWxpdmVyeSBvciByZWxheWluZy4KPiBUaGUKPiArZm9sbG93aW5n IGZvcm1hdHMgYXJlIGN1cnJlbnRseSBzdXBwb3J0ZWQ6Cj4gKwo+ICtAbXVsdGl0YWJsZSB7YWFh YWFhYWFhYWFhYWFhYWFhYX0KPiB7YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh YWFhYWFhYWFhYWFhYWFhYWF9Cj4gK0BpdGVtIEBzYW1weyVAe3NlbmRlckB9fQo+ICtAdGFiIHNl bmRlciBlbWFpbCBhZGRyZXNzLCBtYXkgYmUgZW1wdHkgc3RyaW5nCj4gK0BpdGVtIEBzYW1weyVA e3NlbmRlci51c2VyQH19Cj4gK0B0YWIgdXNlciBwYXJ0IG9mIHRoZSBzZW5kZXIgZW1haWwgYWRk cmVzcywgbWF5IGJlIGVtcHR5Cj4gK0BpdGVtIEBzYW1weyVAe3NlbmRlci5kb21haW5AfX0KPiAr QHRhYiBkb21haW4gcGFydCBvZiB0aGUgc2VuZGVyIGVtYWlsIGFkZHJlc3MsIG1heSBiZSBlbXB0 eQo+ICtAaXRlbSBAc2FtcHslQHtyY3B0QH19Cj4gK0B0YWIgcmVjaXBpZW50IGVtYWlsIGFkZHJl c3MKPiArQGl0ZW0gQHNhbXB7JUB7cmNwdC51c2VyQH19Cj4gK0B0YWIgdXNlciBwYXJ0IG9mIHRo ZSByZWNpcGllbnQgZW1haWwgYWRkcmVzcwo+ICtAaXRlbSBAc2FtcHslQHtyY3B0LmRvbWFpbkB9 fQo+ICtAdGFiIGRvbWFpbiBwYXJ0IG9mIHRoZSByZWNpcGllbnQgZW1haWwgYWRkcmVzcwo+ICtA aXRlbSBAc2FtcHslQHtkZXN0QH19Cj4gK0B0YWIgcmVjaXBpZW50IGVtYWlsIGFkZHJlc3MgYWZ0 ZXIgZXhwYW5zaW9uCj4gK0BpdGVtIEBzYW1weyVAe2Rlc3QudXNlckB9fQo+ICtAdGFiIHVzZXIg cGFydCBhZnRlciBleHBhbnNpb24KPiArQGl0ZW0gQHNhbXB7JUB7ZGVzdC5kb21haW5AfX0KPiAr QHRhYiBkb21haW4gcGFydCBhZnRlciBleHBhbnNpb24KPiArQGl0ZW0gQHNhbXB7JUB7dXNlci51 c2VybmFtZUB9fQo+ICtAdGFiIGxvY2FsIHVzZXIKPiArQGl0ZW0gQHNhbXB7JUB7dXNlci5kaXJl Y3RvcnlAfX0KPiArQHRhYiBob21lIGRpcmVjdG9yeSBvZiB0aGUgbG9jYWwgdXNlcgo+ICtAaXRl bSBAc2FtcHslQHttYm94LmZyb21AfX0KPiArQHRhYiBuYW1lIHVzZWQgaW4gbWJveCBGcm9tIHNl cGFyYXRvciBsaW5lcwo+ICtAaXRlbSBAc2FtcHslQHttZGFAfX0KPiArQHRhYiBtZGEgY29tbWFu ZCwgb25seSBhdmFpbGFibGUgZm9yIG1kYSB3cmFwcGVycwo+ICtAZW5kIG11bHRpdGFibGUKPiAr Cj4gK0V4cGFuc2lvbiBmb3JtYXRzIGFsc28gc3VwcG9ydCBwYXJ0aWFsIGV4cGFuc2lvbiB1c2lu ZyB0aGUgb3B0aW9uYWwKPiBicmFja2V0IG5vdGF0aW9ucwo+ICt3aXRoIHN1YnN0cmluZyBvZmZz ZXQuwqAgRm9yIGV4YW1wbGUsIHdpdGggcmVjaXBpZW50IGRvbWFpbgo+IEBzYW1we+KAnGV4YW1w bGUub3Jn4oCdfToKPiArCj4gK0BtdWx0aXRhYmxlIHthYWFhYWFhYWFhYWFhYWFhYWFhYWFhfSB7 YWFhYWFhYWFhYWFhYWFhYWFhYWF9Cj4gK0BpdGVtIEBzYW1weyVAe3JjcHQuZG9tYWluWzBdQH19 Cj4gK0B0YWIgZXhwYW5kcyB0byDigJxl4oCdCj4gK0BpdGVtIEBzYW1weyVAe3JjcHQuZG9tYWlu WzFdQH19Cj4gK0B0YWIgZXhwYW5kcyB0byDigJx44oCdCj4gK0BpdGVtIEBzYW1weyVAe3JjcHQu ZG9tYWluWzg6XUB9fQo+ICtAdGFiIGV4cGFuZHMgdG8g4oCcb3Jn4oCdCj4gK0BpdGVtIEBzYW1w eyVAe3JjcHQuZG9tYWluWy0zOl1AfX0KPiArQHRhYiBleHBhbmRzIHRvIOKAnG9yZ+KAnQo+ICtA aXRlbSBAc2FtcHslQHtyY3B0LmRvbWFpblswOjZdQH19Cj4gK0B0YWIgZXhwYW5kcyB0byDigJxl eGFtcGxl4oCdCj4gK0BpdGVtIEBzYW1weyVAe3JjcHQuZG9tYWluWzA6LTRdQH19Cj4gK0B0YWIg ZXhwYW5kcyB0byDigJxleGFtcGxl4oCdCj4gK0BlbmQgbXVsdGl0YWJsZQo+ICsKPiArSW4gYWRk aXRpb24sIG1vZGlmaWVycyBtYXkgYmUgYXBwbGllZCB0byB0aGUgdG9rZW4uwqAgRm9yIGV4YW1w bGUsCj4gd2l0aCByZWNpcGllbnQKPiArQHNhbXB74oCcVXNlcitUYWdAQEV4YW1wbGUub3Jn4oCd fToKPiArCj4gK0BtdWx0aXRhYmxlIHthYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWF9Cj4ge2FhYWFh YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYX0KPiArQGl0ZW0gQHNhbXB7JUB7cmNwdDpsb3dl cmNhc2VAfX0KPiArQHRhYiBleHBhbmRzIHRvIOKAnHVzZXIrdGFnQEBleGFtcGxlLm9yZ+KAnQo+ ICtAaXRlbSBAc2FtcHslQHtyY3B0OnVwcGVyY2FzZUB9fQo+ICtAdGFiIGV4cGFuZHMgdG8g4oCc VVNFUitUQUdAQEVYQU1QTEUuT1JH4oCdCj4gK0BpdGVtIEBzYW1weyVAe3JjcHQ6c3RyaXBAfX0K PiArQHRhYiBleHBhbmRzIHRvIOKAnFVzZXJAQEV4YW1wbGUub3Jn4oCdCj4gK0BpdGVtIEBzYW1w eyVAe3JjcHQ6bG93ZXJjYXNlc3RyaXBAfX0KPiArQHRhYiBleHBhbmRzIHRvIOKAnHVzZXJAQGV4 YW1wbGUub3Jn4oCdCj4gK0BlbmQgbXVsdGl0YWJsZQo+ICsKPiArRm9yIHNlY3VyaXR5IGNvbmNl cm5zLCBleHBhbmRlZCB2YWx1ZXMgYXJlIHNhbml0aXplZCBhbmQgcG90ZW50aWFsbHkKPiBkYW5n ZXJvdXMKPiArY2hhcmFjdGVycyBhcmUgcmVwbGFjZWQgd2l0aCDigJg64oCZLiBJbiBzaXR1YXRp b25zIHdoZXJlIHRoZXkgYXJlCj4gZGVzaXJhYmxlLCB0aGUKPiAr4oCccmF34oCdIG1vZGlmaWVy IG1heSBiZSBhcHBsaWVkLiBGb3IgZXhhbXBsZSwgd2l0aCByZWNpcGllbnQKPiArQHNhbXB74oCc dXNlcit0P2dAQGV4YW1wbGUub3Jn4oCdfToKPiArCj4gK0BtdWx0aXRhYmxlIHthYWFhYWFhYWFh YWFhfSB7YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhfQo+ICtAaXRlbSBAc2FtcHsl QHtyY3B0QH19Cj4gK0B0YWIgZXhwYW5kcyB0byDigJx1c2VyK3Q6Z0BAZXhhbXBsZS5vcmfigJ0K PiArQGl0ZW0gQHNhbXB7JUB7cmNwdDpyYXdAfX0KPiArQHRhYiBleHBhbmRzIHRvIOKAnHVzZXIr dD9nQEBleGFtcGxlLm9yZ+KAnQo+ICtAZW5kIG11bHRpdGFibGUKPiArQGVuZCBpdGVtaXplCj4g Kwo+IMKgQHN1YnN1YmhlYWRpbmcgRXhpbSBTZXJ2aWNlCj4gwqAKPiDCoEBjaW5kZXggbWFpbCB0 cmFuc2ZlciBhZ2VudCAoTVRBKQo+IGRpZmYgLS1naXQgYS9nbnUvc2VydmljZXMvbWFpbC5zY20g Yi9nbnUvc2VydmljZXMvbWFpbC5zY20KPiBpbmRleCA0M2YxNDRhNDJkLi5kODZlMDgzZDE5IDEw MDY0NAo+IC0tLSBhL2dudS9zZXJ2aWNlcy9tYWlsLnNjbQo+ICsrKyBiL2dudS9zZXJ2aWNlcy9t YWlsLnNjbQo+IEBAIC01OCwxMCArNTgsMTQ2IEBAIChkZWZpbmUtbW9kdWxlIChnbnUgc2Vydmlj ZXMgbWFpbCkKPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgbWFpbGJveC1jb25maWd1cmF0aW9u Cj4gwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG5hbWVzcGFjZS1jb25maWd1cmF0aW9uCj4gwqAK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtdGFibGUKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCBvcGVuc210cGQtdGFibGU/Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3Bl bnNtdHBkLXRhYmxlLW5hbWUKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtdGFi bGUtZmlsZS1kYgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC10YWJsZS1kYXRh Cj4gKwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1jYQo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1jYT8KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVu c210cGQtY2EtbmFtZQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1jYS1maWxl Cj4gKwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1wa2kKPiArwqDCoMKgwqDC oMKgwqDCoMKgwqDCoCBvcGVuc210cGQtcGtpPwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9w ZW5zbXRwZC1wa2ktZG9tYWluCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLXBr aS1jZXJ0Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLXBraS1rZXkKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtcGtpLWRoZQo+ICsKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCBvcGVuc210cGQtbG9jYWwtZGVsaXZlcnkKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCBvcGVuc210cGQtbG9jYWwtZGVsaXZlcnk/Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAg b3BlbnNtdHBkLWxvY2FsLWRlbGl2ZXJ5LW1ldGhvZAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg IG9wZW5zbXRwZC1sb2NhbC1kZWxpdmVyeS1hbGlhcwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg IG9wZW5zbXRwZC1sb2NhbC1kZWxpdmVyeS10dGwKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBv cGVuc210cGQtbG9jYWwtZGVsaXZlcnktdXNlcgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9w ZW5zbXRwZC1sb2NhbC1kZWxpdmVyeS11c2VyYmFzZQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg IG9wZW5zbXRwZC1sb2NhbC1kZWxpdmVyeS12aXJ0dWFsCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqAgb3BlbnNtdHBkLWxvY2FsLWRlbGl2ZXJ5LXdyYXBwZXIKPiArCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqAgb3BlbnNtdHBkLW1haWxkaXIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVu c210cGQtbWFpbGRpcj8KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtbWFpbGRp ci1wYXRobmFtZQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1tYWlsZGlyLWp1 bmsKPiArCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLW1kYQo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1tZGEtbmFtZQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIG9wZW5zbXRwZC1tZGEtY29tbWFuZAo+ICsKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBv cGVuc210cGQtbG10cAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1sbXRwLWRl c3RpbmF0aW9uCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLWxtdHAtcmNwdAo+ ICsKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtcmVsYXkKPiArwqDCoMKgwqDC oMKgwqDCoMKgwqDCoCBvcGVuc210cGQtcmVsYXk/Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAg b3BlbnNtdHBkLXJlbGF5LWJhY2t1cAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRw ZC1yZWxheS1iYWNrdXAtbXgKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtcmVs YXktaGVsbwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1yZWxheS1kb21haW4K PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtcmVsYXktaG9zdAo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1yZWxheS1wa2kKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCBvcGVuc210cGQtcmVsYXktc3JzCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNt dHBkLXJlbGF5LXRscwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1yZWxheS1h dXRoCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLXJlbGF5LW1haWwtZnJvbQo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1yZWxheS1zcmMKPiArCj4gK8KgwqDC oMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLW9wdGlvbgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIG9wZW5zbXRwZC1vcHRpb24/Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBk LW9wdGlvbi1vcHRpb24KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtb3B0aW9u LW5vdAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1vcHRpb24tcmVnZXgKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtb3B0aW9uLWRhdGEKPiArCj4gK8KgwqDC oMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLWZpbHRlci1waGFzZQo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgIG9wZW5zbXRwZC1maWx0ZXItcGhhc2U/Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqAgb3BlbnNtdHBkLWZpbHRlci1waGFzZS1uYW1lCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAg b3BlbnNtdHBkLWZpbHRlci1waGFzZS1waGFzZS1uYW1lCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqAgb3BlbnNtdHBkLWZpbHRlci1waGFzZS1vcHRpb25zCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqAgb3BlbnNtdHBkLWZpbHRlci1waGFzZS1kZWNpc2lvbgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIG9wZW5zbXRwZC1maWx0ZXItcGhhc2UtbWVzc2FnZQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIG9wZW5zbXRwZC1maWx0ZXItcGhhc2UtdmFsdWUKPiArCj4gK8KgwqDCoMKgwqDCoMKgwqDC oMKgwqAgb3BlbnNtdHBkLWZpbHRlcgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRw ZC1maWx0ZXI/Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLWZpbHRlci1uYW1l Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLWZpbHRlci1wcm9jCj4gKwo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1pbnRlcmZhY2UKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCBvcGVuc210cGQtaW50ZXJmYWNlPwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg IG9wZW5zbXRwZC1pbnRlcmZhY2UtaW50ZXJmYWNlCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAg b3BlbnNtdHBkLWludGVyZmFjZS1mYW1pbHkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVu c210cGQtaW50ZXJmYWNlLWF1dGgKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQt aW50ZXJmYWNlLWF1dGgtb3B0aW9uYWwKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210 cGQtaW50ZXJmYWNlLWZpbHRlcnMKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQt aW50ZXJmYWNlLWhvc3RuYW1lCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLWlu dGVyZmFjZS1ob3N0bmFtZXMKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtaW50 ZXJmYWNlLW1hc2stc3JjCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLWludGVy ZmFjZS1kaXNhYmxlLWRzbgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1pbnRl cmZhY2UtcGtpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLWludGVyZmFjZS1w b3J0Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLWludGVyZmFjZS1wcm94eS12 Mgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1pbnRlcmZhY2UtcmVjZWl2ZWQt YXV0aAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1pbnRlcmZhY2Utc2VuZGVy cwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1pbnRlcmZhY2Utc2VjdXJlLWNv bm5lY3Rpb24KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtaW50ZXJmYWNlLXRh Zwo+ICsKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtc29ja2V0Cj4gK8KgwqDC oMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLXNvY2tldD8KPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCBvcGVuc210cGQtc29ja2V0LWZpbHRlcnMKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBv cGVuc210cGQtc29ja2V0LW1hc2stc3JjCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNt dHBkLXNvY2tldC10YWcKPiArCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLW1h dGNoCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLW1hdGNoPwo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1tYXRjaC1hY3Rpb24KPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCBvcGVuc210cGQtbWF0Y2gtb3B0aW9ucwo+ICsKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCBvcGVuc210cGQtc210cAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1z bXRwPwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1zbXRwLWNpcGhlcnMKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtc210cC1saW1pdC1tYXgtbWFpbHMKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtc210cC1saW1pdC1tYXgtcmNwdAo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1zbXRwLW1heC1tZXNzYWdlLXNpemUKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtc210cC1zdWItYWRkci1kZWxpbSBjaGFy YWN0ZXIKPiArCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLXNycwo+ICvCoMKg wqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1zcnM/Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqAgb3BlbnNtdHBkLXNycy1rZXkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQt c3JzLWJhY2t1cC1rZXkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtc3JzLXR0 bC1kZWxheQo+ICsKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtcXVldWUKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtcXVldWU/Cj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqAgb3BlbnNtdHBkLXF1ZXVlLWNvbXByZXNzaW9uCj4gK8KgwqDCoMKgwqDCoMKgwqDC oMKgwqAgb3BlbnNtdHBkLXF1ZXVlLWVuY3J5cHRpb24KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCBvcGVuc210cGQtcXVldWUtdHRsLWRlbGF5Cj4gKwo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCBvcGVuc210cGQtY29uZmlndXJhdGlvbgo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVu c210cGQtY29uZmlndXJhdGlvbj8KPiAtwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQt c2VydmljZS10eXBlCj4gLcKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgJWRlZmF1bHQtb3BlbnNtdHBk LWNvbmZpZy1maWxlCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLXBhY2thZ2UK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtY29uZmlnLWZpbGUKPiArwqDCoMKg wqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtY29uZmlndXJhdGlvbi1ib3VuY2UKPiArwqDCoMKg wqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQtY29uZmlndXJhdGlvbi1saXN0ZW4tb25zCj4gK8Kg wqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLWNvbmZpZ3VyYXRpb24tbGlzdGVuLW9uLXNv Y2tldAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1jb25maWd1cmF0aW9uLWlu Y2x1ZGVzCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLWNvbmZpZ3VyYXRpb24t bWF0Y2hlcwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1jb25maWd1cmF0aW9u LW1kYS13cmFwcGVycwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1jb25maWd1 cmF0aW9uLW10YS1tYXgtZGVmZXJyZWQKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210 cGQtY29uZmlndXJhdGlvbi1zcnMKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcGVuc210cGQt Y29uZmlndXJhdGlvbi1zbXRwCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLWNv bmZpZ3VyYXRpb24tcXVldWUKPiDCoAo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBtYWlsLWFs aWFzZXMtc2VydmljZS10eXBlCj4gwqAKPiBAQCAtMTY0MSwyMiArMTc3NywxOTQyIEBAIChkZWZp bmUgKGdlbmVyYXRlLWRvdmVjb3QtZG9jdW1lbnRhdGlvbikKPiDCoMKgwqDCoMKgwqDCoCAobGlz dGVuZXJzIHVuaXgtbGlzdGVuZXItY29uZmlndXJhdGlvbiBmaWZvLWxpc3RlbmVyLQo+IGNvbmZp Z3VyYXRpb24KPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgaW5ldC1saXN0 ZW5lci1jb25maWd1cmF0aW9uKSkKPiDCoMKgwqDCoMKgwqAgKHByb3RvY29sLWNvbmZpZ3VyYXRp b24gLHByb3RvY29sLWNvbmZpZ3VyYXRpb24tZmllbGRzKSkKPiAtwqAgJ2RvdmVjb3QtY29uZmln dXJhdGlvbikpCj4gK8KgwqAgJ2RvdmVjb3QtY29uZmlndXJhdGlvbikpCj4gwqAKPiDCoAwKPiDC oDs7Owo+IMKgOzs7IE9wZW5TTVRQRC4KPiDCoDs7Owo+IMKgCj4gKzs7IGZpbGUtZXhpc3RzPyBp cyBpbiB0aGUgZ3VpbGUgc3RhbmRhcmQgbGlicmFyeS7CoCBCVVQgSSBlcnJvcnMgaWYKPiBpdHMg YXJnCj4gKzs7IGlzIGEgbGlzdC7CoCBlZzrCoCAoZmlsZS1leGlzdHM/IChsaXN0ICJoZWxsbyIg ImhlbGxvIikpCj4gKzs7IFRPRE8gSSBuZWVkIHRvIGZpbmQgYSB3YXkgdG8gcmVtb3ZlIHRoaXMg ZGVmaW5pdGlvbiBhbmQgcmV3cml0ZQo+IG15IGNvZGUuCj4gKyhkZWZpbmUgKGZpbGUtZXhpc3Rz PyBmaWxlKQo+ICvCoCAoaWYgKHN0cmluZz8gZmlsZSkKPiArwqDCoMKgwqDCoCAoYWNjZXNzPyBm aWxlIEZfT0spCj4gK8KgwqDCoMKgwqAgI2YpKQo+ICsKPiArOzsgc29tZSBmaWVsZG5hbWVzIGhh dmUgYSBkZWZhdWx0IHZhbHVlIG9mICNmLCB3aGljaCBpcyBvay7CoCBUaGV5Cj4gY2Fubm90IGhh dmUgYSB2YWx1ZSBvZiAjdC4KPiArOzsgZm9yIGV4YW1wbGUgb3BlbnNtdHBkLXRhYmxlLWRhdGEg Y2FuIGJlICNmLCBCVVQgTk9UIHRydWUuCj4gKzs7IG15L3Nhbml0aXplIHByb2NlZHVyZSB0ZXN0 cyB2YWx1ZXMgdG8gc2VlIGlmIHRoZXkgYXJlIG9mIHRoZQo+IHJpZ2h0IGtpbmQuCj4gKzs7IHBy b2NlZHVyZSBmYWxzZT8gaXMgbmVlZGVkIHRvIGFsbG93IGZpZWxkcyBsaWtlICd2YWx1ZXMnIHRv IGJlCj4gYmxhbmssIChlbXB0eSksIG9yICNmIEJVVCBhbHNvCj4gKzs7IGhhdmUgYSB2YWx1ZSBs aWtlIGEgbGlzdCBvZiBzdHJpbmdzLgo+ICsoZGVmaW5lIChmYWxzZT8gdmFyKQo+ICvCoCAoZXE/ ICNmIHZhcikpCj4gKwo+ICs7OyB0aGlzIHByb2NlZHVyZSB0YWtlcyBpbiBhIHZhciBhbmQgYSBs aXN0IG9mIHByb2NlZHVyZXMuwqAgSXQgbG9vcHMKPiB0aHJvdWdoIGxpc3Qgb2YgcHJvY2VkdXJl cyBwYXNzaW5nIGluIHZhciB0byBlYWNoLgo+ICs7OyBpZiBvbmUgcHJvY2VkdXJlIHJldHVybnMg I3QsIHRoZSBmdW5jdGlvbiByZXR1cm5zIHRydWUuwqAKPiBPdGhlcndpc2UgI2YuCj4gKzs7IFRP RE8gZm9yIGZ1biByZXdyaXRlIHRoaXMgdXNpbmcgbWFwCj4gKzs7IElmIEkgcmV3cm90ZSBpdCBp biBtYXAsIHRoZW4gaXQgbWF5IGhlbHAgd2l0aCBzYW5pdGl6aW5nLgo+ICs7OyBlZzogSSBjb3Vs ZCB0aGVuIHBvdGVudGlhbGx5IGVhc2lseSBzYW5pdGl6ZSB2YXJzIHdpdGggbGFtYmRhCj4gcHJv Y2VkdXJlcy4KPiArKGRlZmluZSAoaXMtdmFsdWUtcmlnaHQtdHlwZT8gdmFyIGxpc3Qtb2YtcHJv Y2VkdXJlcyByZWNvcmQKPiBmaWVsZG5hbWUpCj4gK8KgIChpZiAobnVsbD8gbGlzdC1vZi1wcm9j ZWR1cmVzKQo+ICvCoMKgwqDCoMKgICNmCj4gK8KgwqDCoMKgwqAgKGlmICgoY2FyIGxpc3Qtb2Yt cHJvY2VkdXJlcykgdmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqAgI3QKPiArwqDCoMKgwqDCoMKg wqDCoMKgIChpcy12YWx1ZS1yaWdodC10eXBlPyB2YXIgKGNkciBsaXN0LW9mLXByb2NlZHVyZXMp IHJlY29yZAo+IGZpZWxkbmFtZSkpKSkKPiArCj4gKzs7IGNvbnZlcnRzIHN0cmluZ3MgbGlrZSB0 aGlzOgo+ICs7OyAiYXBwbGUsIGhhbSwgY2hlcnJ5IiAtPiAiYXBwbGUsIGhhbSwgb3IgY2hlcnJ5 Igo+ICs7OyAicGluZWFwcGxlIiAtPiAicGlubmVhcHBsZSIuCj4gKzs7ICJjaGVlc2UsIGdyYXBl ZnJ1aXQsIG9yIGphbSIgLT4gImNoZWVzZSwgZ3JhcGVmcnVpdCwgb3IgamFtIgo+ICsoZGVmaW5l IChhZGQtY29tbWEtb3Igc3RyaW5nKQo+ICvCoCAoZGVmaW5lIGxhc3QtY29tbWEtbG9jYXRpb24g KHN0cmluZy1yaW5kZXggc3RyaW5nICNcLCkpCj4gK8KgIChpZiBsYXN0LWNvbW1hLWxvY2F0aW9u Cj4gK8KgwqDCoMKgwqAgKGlmIChzdHJpbmctY29udGFpbnMgc3RyaW5nICIsIG9yIiBsYXN0LWNv bW1hLWxvY2F0aW9uKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqAgc3RyaW5nCj4gK8KgwqDCoMKgwqDC oMKgwqDCoCAoc3RyaW5nLXJlcGxhY2Ugc3RyaW5nICIsIG9yIiBsYXN0LWNvbW1hLWxvY2F0aW9u Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICgr IDEgbGFzdC1jb21tYS1sb2NhdGlvbikpKQo+ICvCoMKgwqDCoMKgIHN0cmluZykpCj4gKwo+ICso ZGVmaW5lIChsaXN0LW9mLXByb2NlZHVyZXMtPnN0cmluZyBwcm9jZWR1cmVzKQo+ICvCoCAoZGVm aW5lIHN0cmluZwo+ICvCoMKgwqAgKGxldCBsb29wICgocHJvY2VkdXJlcyBwcm9jZWR1cmVzKSkK PiArwqDCoMKgwqDCoCAoaWYgKG51bGw/IHByb2NlZHVyZXMpCj4gK8KgwqDCoMKgwqDCoMKgwqDC oCAiIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqAgKGJlZ2luCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqAgKHN0cmluZy1hcHBlbmQKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChjb25kICgoZXE/ IGZhbHNlPyAoY2FyIHByb2NlZHVyZXMpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAiI2YgLCAiKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgKChlcT8gYm9vbGVhbj8gKGNhciBwcm9jZWR1cmVzKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqAgImJvb2xlYW4sICIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoCAoKGVxPyBzdHJpbmc/IChjYXIgcHJvY2VkdXJlcykpCj4gK8KgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJzdHJpbmcsICIpCj4gK8KgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoKGVxPyBpbnRlZ2VyPyAoY2FyIHByb2NlZHVy ZXMpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAiaW50ZWdlciwg IikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICgoZXE/IGxpc3Qtb2Yt c3RyaW5ncz8gKGNhciBwcm9jZWR1cmVzKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgImxpc3Qgb2Ygc3RyaW5ncywgIikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgICgoZXE/IGFzc29jLWxpc3Q/IChjYXIgcHJvY2VkdXJlcykpCj4gK8Kg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJhbiBhc3NvY2lhdGlvbiBsaXN0 LCAiKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKChlcT8gb3BlbnNt dHBkLXBraT8gKGNhciBwcm9jZWR1cmVzKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgImFuIDxvcGVuc210cGQtcGtpPiByZWNvcmQsICIpCj4gK8KgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoKGVxPyBvcGVuc210cGQtdGFibGU/IChjYXIgcHJv Y2VkdXJlcykpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJhbiA8 b3BlbnNtdHBkLXRhYmxlPiByZWNvcmQsICIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAoKGVxPyBsaXN0LW9mLXVuaXF1ZS1vcGVuc210cGQtbWF0Y2g/IChjYXIKPiBw cm9jZWR1cmVzKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgImEg bGlzdCBvZiB1bmlxdWUgPG9wZW5zbXRwZC1tYXRjaD4gcmVjb3JkcywgIikKPiArwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICgoZXE/IGxpc3Qtb2Ytc3RyaW5ncy1vci1nZXhw cz8gKGNhciBwcm9jZWR1cmVzKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgImEgbGlzdCBvZiBzdHJpbmdzIG9yIGdleHBzLCAiKQo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqAgKChlcT8gdGFibGUtd2hvc2UtZGF0YS1hcmUtYXNzb2MtbGlz dD8gKGNhcgo+IHByb2NlZHVyZXMpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAoc3RyaW5nLWFwcGVuZAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgICJhbiA8b3BlbnNtdHBkLXRhYmxlPiByZWNvcmQgd2hvc2UgZmllbGRuYW1lCj4g J2RhdGEnIGFyZSBhbiBhc3NvYy1saXN0IFxuIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgICIoZWc6IChvcGVuc210cGQtdGFibGUgKG5hbWUgXCJob3N0bmFtZXNc IikKPiAoZGF0YSAnKChcIjEyNC4zOTQuMjMuMVwiIC4gXCJnbnUub3JnXCIpKSkpKSwgIikpCj4g K8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoKGVxPyBmaWxlLWV4aXN0cz8g KGNhciBwcm9jZWR1cmVzKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgImZpbGUsICIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZWxz ZSAiaGFzIGFuIGluY29ycmVjdCB2YWx1ZSwgIikpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCAobG9vcCAoY2RyIHByb2NlZHVyZXMpKSkpKSkpCj4gK8KgIChhZGQtY29tbWEtb3IgKHN0cmlu Zy1hcHBlbmQgKHN0cmluZy1kcm9wLXJpZ2h0IHN0cmluZyAyKSAiLlxuIikpKQo+ICsKPiArKGRl ZmluZSAoc3RyaW5nLWluLWxpc3Q/IHN0cmluZyBsaXN0KQo+ICvCoCAobWVtYmVyIHN0cmluZyBs aXN0KSkKPiArCj4gKyhkZWZpbmUgKGxpc3Qtb2Ytc3RyaW5ncy1vci1nZXhwcz8gbGlzdCkKPiAr wqAgKGFuZCAobGlzdD8gbGlzdCkKPiArwqDCoMKgwqDCoMKgIChjb25kICgobnVsbD8gbGlzdCkK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgI3QpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCAoKG9yIChzdHJpbmc/IChjYXIgbGlzdCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgKGdleHA/IChjYXIgbGlzdCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgKGxvY2FsLWZpbGU/IChjYXIgbGlzdCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgKGZpbGUtYXBwZW5kPyAoY2FyIGxpc3QpKQo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChwbGFpbi1maWxlPyAoY2FyIGxpc3QpKQo+ICvCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChjb21wdXRlZC1maWxlPyAoY2FyIGxpc3Qp KQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChwcm9ncmFtLWZpbGU/IChj YXIgbGlzdCkpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdC1vZi1zdHJpbmdz LW9yLWdleHBzPyAoY2RyIGxpc3QpKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChlbHNl ICNmKSkpKQo+ICsKPiArKGRlZmluZSAobXkvc2FuaXRpemUgdmFyIHJlY29yZCBmaWVsZG5hbWUg bGlzdC1vZi1wcm9jZWR1cmVzKQo+ICvCoCAoaWYgKGlzLXZhbHVlLXJpZ2h0LXR5cGU/IHZhciBs aXN0LW9mLXByb2NlZHVyZXMgcmVjb3JkIGZpZWxkbmFtZSkKPiArwqDCoMKgwqDCoCB2YXIKPiAr wqDCoMKgwqDCoCAoYmVnaW4KPiArwqDCoMKgwqDCoMKgwqAgKGRpc3BsYXkgKHN0cmluZy1hcHBl bmQgIjwiIHJlY29yZCAiPiBmaWVsZG5hbWU6ICciCj4gZmllbGRuYW1lICInIGlzIG9mIHR5cGUg Igo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAobGlzdC1vZi1wcm9jZWR1cmVzLT5zdHJpbmcgbGlzdC1vZi0KPiBwcm9jZWR1 cmVzKSAiXG4iKSkKPiArwqDCoMKgwqDCoMKgwqAgKHRocm93ICdiYWQhIHZhcikpKSkKVGhhdCdz IGEgcmF0aGVyIGNydWRlIHdheSBvZiBzYW5pdGl6aW5nLiAgWW91IHNob3VsZCBwcm9iYWJseSBy YWlzZSBhCmZvcm1hdHRlZC1tZXNzYWdlIG9yIHNpbWlsYXIuICBJJ2QgYWxzbyBjdXJyeSB0aGlz IGFzIGZvbGxvd3M6CgooZGVmaW5lICgoKGV4cGVjdC1hbnkgcHJlZGljYXRlcykgcmVjb3JkIGZp ZWxkKSB2YXIpCiAgKGlmIChhbnkgKGN1dGUgPD4gdmFyKSBwcmVkaWNhdGVzKQogICAgICB2YXIK ICAgICAgKGRvLXRoZS1leGNlcHRpb24tcmFpc2luZykpKQoKd2hlcmUgZG8tdGhlLWV4Y2VwdGlv bi1yYWlzaW5nIGNvbnRhaW5zIGFsbCB0aGUgZm9ybWF0dGluZyBzdHVmZiBldGMuCnRoYXQgSSBz a2lwcGVkIGZvciB0aGUgc2FrZSBvZiBzaW1wbGljaXR5LgoKVGhlbiB5b3UgY2FuIGRlZmluZSAo ZXhwZWN0LXN0cmluZy1vci0jZikgYW5kIHdoYXRldmVyIGVsc2UgeW91IG5lZWQKcXVpdGUgc2lt cGx5LgoKPiArOzsgU29tZSBleGFtcGxlIG9wZW5zbXRwZC10YWJsZXM6Cj4gKzs7Cj4gKzs7wqAg KG9wZW5zbXRwZC10YWJsZSAobmFtZSAicm9vdCBhY2NvdW50cyIpIChkYXRhICcoKCJqb3NodWEi IC4KPiAicm9vdEBkaXNtYWlsLmRlIikgKCJqb3NodWEiIC4gInBvc3RtYXN0ZXJAZGlzbWFpbC5k ZSIpKSkpCj4gKzs7wqAgKG9wZW5zbXRwZC10YWJsZSAobmFtZSAicm9vdCBhY2NvdW50cyIpIChk YXRhIChsaXN0ICJteXNpdGUubWUiCj4gInlvdXItc2l0ZS5jb20iKSkpCj4gKzs7wqAgVE9ETyBz aG91bGQgPG9wZW5zbXRwZC10YWJsZT4gc3VwcG9ydCBoYXZlIGEgZmllbGRuYW1lICdmaWxlJz8K PiArOzvCoCBPciBzaG91bGQgSSBjaGFuZ2UgbmFtZSB0byBuYW1lLW9yLWZpbGUgPwo+ICsoZGVm aW5lLXJlY29yZC10eXBlKiA8b3BlbnNtdHBkLXRhYmxlPgo+ICvCoCBvcGVuc210cGQtdGFibGUg bWFrZS1vcGVuc210cGQtdGFibGUKPiArwqAgb3BlbnNtdHBkLXRhYmxlPwo+ICvCoCB0aGlzLXJl Y29yZAo+ICvCoCAobmFtZSBvcGVuc210cGQtdGFibGUtbmFtZSA7OyBzdHJpbmcKPiArwqDCoMKg wqDCoMKgwqAgKGRlZmF1bHQgI2YpCj4gK8KgwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFtYmRh ICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChteS9zYW5p dGl6ZSB2YXIgIm9wZW5zbXRwZC10YWJsZSIgIm5hbWUiIChsaXN0Cj4gc3RyaW5nPykpKSkpCj4g K8KgIChmaWxlLWRiIG9wZW5zbXRwZC10YWJsZS1maWxlLWRiCj4gK8KgwqDCoMKgwqDCoMKgwqDC oMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc2FuaXRpemUgKGxhbWJk YSAodmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAo bXkvc2FuaXRpemUgdmFyICJvcGVuc210cGQtdGFibGUiICJmaWxlLWRiIgo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIChsaXN0IGJvb2xlYW4/KSkpKSkKPiArwqAgOzsgRklYTUUgc3VwcG9ydCBhbiBhbGlhc2lu ZyB0YWJsZSBhcyBkZXNjcmliZWQgaGVyZToKPiArwqAgOzsgaHR0cHM6Ly9tYW4ub3BlbmJzZC5v cmcvdGFibGUuNQo+ICvCoCA7OyBPbmUgbWF5IGhhdmUgdG8gdXNlIHRoZSByZWNvcmQgZmlsZSBm b3IgdGhpcy7CoCBJIGRvbid0IHRoaW5rCj4gdGFibGVzIHN1cHBvcnQgYSB0YWJsZSBsaWtlIHRo aXM6Cj4gK8KgIDs7IHRhYmxlICJuYW1lIiB7IGpvc2h1YSA9Cj4gam9zaHVhQGdudWNvZGUubWUs am9zaHVhQGdudS1odXJkLmNvbSxqb3NodWFAcHJvcGVybmFtaW5nLm9yZywgcm9vdCA9Cj4gcm9v dEBnbnVjb2RlLm1lwqB9Cj4gK8KgIDs7IElmIHZhbHVlcyBpcyBhbiBhYnNvbHV0ZSBmaWxlbmFt ZSwgdGhlbiBpdCB3aWxsIHVzZSBzYWlkCj4gZmlsZW5hbWUgdG8gaG91c2UgdGhlIHRhYmxlIGlu Zm8uCj4gK8KgIDs7IGZpbGVuYW1lIG11c3QgYmUgYW4gYWJzb2x1dGUgZmlsZW5hbWUuCj4gK8Kg IChkYXRhIG9wZW5zbXRwZC10YWJsZS1kYXRhCj4gK8KgwqDCoMKgwqDCoMKgwqDCoCAoZGVmYXVs dCAjZikKPiArwqDCoMKgwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFtYmRhICh2YXIpCj4gK8Kg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2FuaXRpemUgdmFy ICJvcGVuc210cGQtdGFibGUiICJ2YWx1ZXMiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsaXN0IGxpc3Qtb2Yt c3RyaW5ncz8gYXNzb2MtCj4gbGlzdD8gZmlsZS1leGlzdHM/KSkpKSkKPiArwqAgOzsgaXMgYSBs aXN0IG9mIHZhbHVlcyBvciBrZXkgdmFsdWVzCj4gK8KgIDs7IGVnOiAobGlzdCAibXlzaXRlLm1l IiAieW91ci1zaXRlLmNvbSIpCj4gK8KgIDs7IGVnOiAobGlzdCAoImpvc2h1YSIgLiAiam9zaHVh QGdudS5vcmciKSAoImphbWVzIiAuCj4gImphbWVzQGdudS5vcmciKSkKPiArwqAgOzsgSSBhbSBj dXJyZW50bHkgbWFraW5nIHRoZXNlIHZhbHVlcyBiZSBhcyBhc3NvY2F0aW9uIGxpc3Qgb2YKPiBz dHJpbmdzIG9ubHkuCj4gK8KgIDs7IEZJWE1FIHNob3VsZCBJIGFsbG93IGEgdmFyIGxpa2UgdGhp cz8KPiArwqAgOzsgKGxpc3QgKGNvbnMgImdudWNvZGUubWUiIDIzNC45NDkuMzkyLjIzKSkKPiAr wqAgOzsgY2FuIGJlIG9mIHR5cGU6IChxdW90ZSBsaXN0LW9mLXN0cmluZ3MpIG9yIChxdW90ZSBh c3NvYy1saXN0KQo+ICvCoCA7OyAob3BlbnNtdHBkLXRhYmxlLXR5cGUgcmVjb3JkKSByZXR1cm5z IHRoZSB2YWx1ZXMnIHR5cGUuwqAgVGhlCj4gdXNlciBTSE9VTEQgTkVWRVIgc2V0IHRoZSB0eXBl Lgo+ICvCoCA7OyBUT0RPIGpwb2lyZXQ6IG9uIGlyYyByZWNjb21lbmRzIHRoYXQgSSBqdXN0IHVz ZSBhbiBvdXRzaWRlCj4gZnVuY3Rpb24gdG8gZGV0ZXJtaW5lIGZpZWxkbmFtZSAndmFsdWVzJywg dHlwZS4KPiArwqAgOzsgaXQgd291bGQgYmUgInNpbXBsZXIiIGFuZCBwb3NzaWJseSBlYXNpZXIg Zm9yIHRoZSBuZXh0IHBlcnNvbgo+IHdvcmtpbmcgb24gdGhpcyBjb2RlIHRvIHVuZGVyc3RhbmQg d2hhdCBpcyBoYXBwZW5pbmcuCj4gK8KgICh0eXBlIG9wZW5zbXRwZC10YWJsZS10eXBlCj4gK8Kg wqDCoMKgwqDCoMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqDCoCAodGh1bmtlZCkKPiAr wqDCoMKgwqDCoMKgwqAgKHNhbml0aXplIChsYW1iZGEgKHZhcikKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGNvbmQgKChvcGVuc210cGQtdGFibGUtZGF0YSB0aGlz LXJlY29yZCkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAoaWYgKGxpc3Qtb2Ytc3RyaW5ncz8gKG9wZW5zbXRwZC10YWJsZS0KPiBkYXRhIHRo aXMtcmVjb3JkKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgIChxdW90ZSBsaXN0LW9mLXN0cmluZ3MpCj4gK8KgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAocXVvdGUg YXNzb2MtbGlzdCkpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAoKGZpbGUtZXhpc3RzPyAob3BlbnNtdHBkLXRhYmxlLWRhdGEgdGhpcy0KPiBy ZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgIChpZiAob3BlbnNtdHBkLXRhYmxlLWZpbGUtZGIgdGhpcy1yZWNvcmQpCj4gK8KgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAo cXVvdGUgZGIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoCAocXVvdGUgZmlsZSkpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZWxzZQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChkaXNwbGF5ICJvcGVuc210cGQtdGFi bGUtdHlwZSBpcwo+IGJyb2tlXG4iKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgICh0aHJvdyAnYmFkISB2YXIpKSkpKSkpCj4gKwo+ICsoZGVm aW5lLXJlY29yZC10eXBlKiA8b3BlbnNtdHBkLWNhPgo+ICvCoCBvcGVuc210cGQtY2EgbWFrZS1v cGVuc210cGQtY2EKPiArwqAgb3BlbnNtdHBkLWNhPwo+ICvCoCAobmFtZSBvcGVuc210cGQtY2Et bmFtZQo+ICvCoMKgwqDCoMKgwqDCoCAoZGVmYXVsdCAjZikKPiArwqDCoMKgwqDCoMKgwqAgKHNh bml0aXplIChsYW1iZGEgKHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKG15L3Nhbml0aXplIHZhciAib3BlbnNtdHBkLWNhIiAibmFtZSIgKGxpc3QKPiBzdHJp bmc/KSkpKSkKPiArwqAgKGZpbGUgb3BlbnNtdHBkLWNhLWZpbGUKPiArwqDCoMKgwqDCoMKgwqAg KGRlZmF1bHQgI2YpCj4gK8KgwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFtYmRhICh2YXIpCj4g K8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChteS9zYW5pdGl6ZSB2YXIg Im9wZW5zbXRwZC1jYSIgImZpbGUiIChsaXN0Cj4gZmlsZS1leGlzdHM/KSkpKSkpCj4gKwo+ICso ZGVmaW5lLXJlY29yZC10eXBlKiA8b3BlbnNtdHBkLXBraT4KPiArwqAgb3BlbnNtdHBkLXBraSBt YWtlLW9wZW5zbXRwZC1wa2kKPiArwqAgb3BlbnNtdHBkLXBraT8KPiArwqAgKGRvbWFpbiBvcGVu c210cGQtcGtpLWRvbWFpbgo+ICvCoMKgwqDCoMKgwqDCoMKgwqAgKGRlZmF1bHQgI2YpCj4gK8Kg wqDCoMKgwqDCoMKgwqDCoCAoc2FuaXRpemUgKGxhbWJkYSAodmFyKQo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG15L3Nhbml0aXplIHZhciAib3BlbnNtdHBk LXBraSIgImRvbWFpbiIKPiAobGlzdCBzdHJpbmc/KSkpKSkKPiArwqAgOzsgVE9ETy9GSVhNRSB0 aGlzIHNob3VsZCBwcm9iYWJseSBiZSBhIGxpc3Qgb2YgZmlsZXMuwqAgVGhlCj4gb3BlbnNtdHBk IGRvY3VtZW50YXRpb24gc2F5cwo+ICvCoCA7OyB0aGF0IHlvdSBjb3VsZCBoYXZlIGEgbGlzdCBv ZiBmaWxlczoKPiArwqAgOzsKPiArwqAgOzsgcGtpIHBraW5hbWUgY2VydCBjZXJ0ZmlsZQo+ICvC oCA7OyBBc3NvY2lhdGUgY2VydGlmaWNhdGUgZmlsZSBjZXJ0ZmlsZSB3aXRoIGhvc3QgcGtpbmFt ZSwgYW5kIHVzZQo+IHRoYXQgZmlsZSB0byBwcm92ZQo+ICvCoCA7OyB0aGUgaWRlbnRpdHkgb2Yg dGhlIG1haWwgc2VydmVyIHRvIGNsaWVudHMuwqAgcGtpbmFtZSBpcyB0aGUKPiBzZXJ2ZXIncyBu YW1lLCBkZeKAkAo+ICvCoCA7OyByaXZlZCBmcm9tIHRoZSBkZWZhdWx0IGhvc3RuYW1lIG9yIHNl dCB1c2luZyBlaXRoZXIKPiArwqAgOzsgL2dudS9zdG9yZS8yZDEzc2R6NzZsZHE4emd3djR3aWYw eng3aGtyM21oMi1vcGVuc210cGQtCj4gNi44LjBwMi9ldGMvbWFpbG5hbWUgb3IgdXPigJAKPiAr wqAgOzsgaW5nIHRoZSBob3N0bmFtZSBkaXJlY3RpdmUuwqAgSWYgYSBmYWxsYmFjayBjZXJ0aWZp Y2F0ZSBvciBTTkkKPiBpcyB3YW50ZWQsIHRoZSDigJgq4oCZCj4gK8KgIDs7IHdpbGRjYXJkIG1h eSBiZSB1c2VkIGFzIHBraW5hbWUuCj4gKwo+ICvCoCA7OyBBIGNlcnRpZmljYXRlIGNoYWluIG1h eSBiZSBjcmVhdGVkIGJ5IGFwcGVuZGluZyBvbmUgb3IgbWFueQo+IGNlcnRpZmljYXRlcywgaW5j bHVk4oCQCj4gK8KgIDs7IGluZyBhIENlcnRpZmljYXRlIEF1dGhvcml0eSBjZXJ0aWZpY2F0ZSwg dG8gY2VydGZpbGUuwqAgVGhlCj4gY3JlYXRpb24gb2YgY2VydGlmaeKAkAo+ICvCoCA7OyBjYXRl cyBpcyBkb2N1bWVudGVkIGluIHN0YXJ0dGxzKDgpLgo+ICvCoCAoY2VydCBvcGVuc210cGQtcGtp LWNlcnQKPiArwqDCoMKgwqDCoMKgwqAgKGRlZmF1bHQgI2YpCj4gK8KgwqDCoMKgwqDCoMKgIChz YW5pdGl6ZSAobGFtYmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgIChteS9zYW5pdGl6ZSB2YXIgIm9wZW5zbXRwZC1wa2kiICJjZXJ0IiAobGlzdAo+IGZp bGUtZXhpc3RzPykpKSkpCj4gK8KgIChrZXkgb3BlbnNtdHBkLXBraS1rZXkKPiArwqDCoMKgwqDC oMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqAgKHNhbml0aXplIChsYW1iZGEgKHZhcikK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChteS9zYW5pdGl6ZSB2YXIg Im9wZW5zbXRwZC1wa2kiICJrZXkiIChsaXN0Cj4gZmlsZS1leGlzdHM/KSkpKSkKPiArwqAgOyB0 b2RvIHNhbml0aXplIHRoaXMuIHZhbGlkIHBhcmFtZXRlcnMgYXJlICJub25lIiwgImxlZ2FjeSIs IG9yCj4gImF1dG8iLgo+ICvCoCAoZGhlIG9wZW5zbXRwZC1wa2ktZGhlCj4gK8KgwqDCoMKgwqDC oCAoZGVmYXVsdCAjZikKPiArwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFtYmRhICh2YXIpCj4g K8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2FuaXRpemUgdmFyICJv cGVuc210cGQtZGhlIiAiZGhlIiAobGlzdAo+IGZhbHNlPyBzdHJpbmc/KSkpKSkpCj4gKwo+ICso ZGVmaW5lLXJlY29yZC10eXBlKiA8b3BlbnNtdHBkLWxtdHA+Cj4gK8KgIG9wZW5zbXRwZC1sbXRw IG1ha2Utb3BlbnNtdHBkLWxtdHAKPiArwqAgb3BlbnNtdHBkLWxtdHA/Cj4gK8KgIChkZXN0aW5h dGlvbiBvcGVuc210cGQtbG10cC1kZXN0aW5hdGlvbgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzYW5p dGl6ZSAobGFtYmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgKG15L3Nhbml0aXplIHZhciAib3BlbnNtdHBkLWxtdHAiCj4gImRl c3RpbmF0aW9uIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3Qgc3RyaW5nPykpKSkpCj4g K8KgIChyY3B0LXRvIG9wZW5zbXRwZC1sbXRwLXJjcHQtdG8KPiArwqDCoMKgwqDCoMKgwqDCoMKg wqAgKGRlZmF1bHQgI2YpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFtYmRh ICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICht eS9zYW5pdGl6ZSB2YXIgIm9wZW5zbXRwZC1sbXRwIiAicmNwdC10byIKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCAobGlzdCBmYWxzZT8gc3RyaW5nPykpKSkpKQo+ICsKPiArKGRlZmluZS1yZWNvcmQtdHlwZSog PG9wZW5zbXRwZC1tZGE+Cj4gK8KgIG9wZW5zbXRwZC1tZGEgbWFrZS1vcGVuc210cGQtbWRhCj4g K8KgIG9wZW5zbXRwZC1tZGE/Cj4gK8KgIChuYW1lIG9wZW5zbXRwZC1tZGEtbmFtZQo+ICvCoMKg wqDCoMKgwqDCoCAoZGVmYXVsdCAjZikKPiArwqDCoMKgwqDCoMKgwqAgKHNhbml0aXplIChsYW1i ZGEgKHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG15L3Nh bml0aXplIHZhciAib3BlbnNtdHBkLW1kYSIgIm5hbWUiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3Qgc3RyaW5n PykpKSkpCj4gK8KgIDs7IFRPRE8gc2hvdWxkIEkgYWxsb3cgdGhpcyBjb21tYW5kIHRvIGJlIGEg Z2V4cD8KPiArwqAgKGNvbW1hbmQgb3BlbnNtdHBkLW1kYS1jb21tYW5kCj4gK8KgwqDCoMKgwqDC oMKgwqDCoMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc2FuaXRpemUg KGxhbWJkYSAodmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCAobXkvc2FuaXRpemUgdmFyICJvcGVuc210cGQtbWRhIiAiY29tbWFuZCIKPiArwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAobGlzdCBzdHJpbmc/KSkpKSkpCj4gKwo+ICsoZGVmaW5lLXJlY29yZC10eXBlKiA8 b3BlbnNtdHBkLW1haWxkaXI+Cj4gK8KgIG9wZW5zbXRwZC1tYWlsZGlyIG1ha2Utb3BlbnNtdHBk LW1haWxkaXIKPiArwqAgb3BlbnNtdHBkLW1haWxkaXI/Cj4gK8KgIChwYXRobmFtZSBvcGVuc210 cGQtbWFpbGRpci1wYXRobmFtZQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChkZWZhdWx0ICNm KQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFtYmRhICh2YXIpCj4gK8Kg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG15L3Nhbml0aXpl IHZhciAib3BlbnNtdHBkLW1haWxkaXIiCj4gInBhdGhuYW1lIgo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAg KGxpc3QgZmFsc2U/IHN0cmluZz8pKSkpKQo+ICvCoCAoanVuayBvcGVuc210cGQtbWFpbGRpci1q dW5rCj4gK8KgwqDCoMKgwqDCoMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqDCoCAoc2Fu aXRpemUgKGxhbWJkYSAodmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCAobXkvc2FuaXRpemUgdmFyICJvcGVuc210cGQtbWFpbGRpciIgImp1bmsiCj4gK8KgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgKGxpc3QgYm9vbGVhbj8pKSkpKSkKPiArCj4gKyhkZWZpbmUtcmVjb3JkLXR5cGUqIDxvcGVu c210cGQtbG9jYWwtZGVsaXZlcnk+Cj4gK8KgIG9wZW5zbXRwZC1sb2NhbC1kZWxpdmVyeSBtYWtl LW9wZW5zbXRwZC1sb2NhbC1kZWxpdmVyeQo+ICvCoCBvcGVuc210cGQtbG9jYWwtZGVsaXZlcnk/ Cj4gK8KgIChuYW1lIG9wZW5zbXRwZC1sb2NhbC1kZWxpdmVyeS1uYW1lCj4gK8KgwqDCoMKgwqDC oMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqDCoCAoc2FuaXRpemUgKGxhbWJkYSAodmFy KQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2FuaXRpemUg dmFyICJvcGVuc210cGQtbG9jYWwtZGVsaXZlcnkiCj4gIm5hbWUiCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3Qg c3RyaW5nPykpKSkpCj4gK8KgIChtZXRob2Qgb3BlbnNtdHBkLWxvY2FsLWRlbGl2ZXJ5LW1ldGhv ZAo+ICvCoMKgwqDCoMKgwqDCoMKgwqAgKGRlZmF1bHQgIm1ib3giKQo+ICvCoMKgwqDCoMKgwqDC oMKgwqAgKHNhbml0aXplIChsYW1iZGEgKHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgIChjb25kCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgICgob3IgKG9wZW5zbXRwZC1sbXRwPyB2YXIpCj4gK8KgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3BlbnNtdHBkLW1h aWxkaXI/IHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIChvcGVuc210cGQtbWRhPyB2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc3RyaW5nPT8gdmFyICJtYm94IikK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg IChzdHJpbmc9PyB2YXIgImV4cGFuZC1vbmx5IikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzdHJpbmc9PyB2YXIgImZvcndhcmQtb25s eSIpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIHZh cikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGVsc2UK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoYmVnaW4K PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGRp c3BsYXkgKHN0cmluZy1hcHBlbmQgIjxvcGVuc210cGQtbG9jYWwtCj4gZGVsaXZlcnk+IGZpZWxk bmFtZSAnbWV0aG9kJyBtdXN0IGJlIG9mIHR5cGUgXG4iCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgICJcIm1ib3hcIiwKPiBcImV4cGFuZC1vbmx5XCIsIFwiZm9y d2FyZC1vbmx5XCIgXG4iCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgICI8b3BlbnNtdHBkLWxtdHA+LAo+IDxvcGVuc210cGQtbWFpbGRpcj4sIFxuIgo+ICvCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAib3IgPG9wZW5zbXRwZC0KPiBt ZGE+LlxuIikpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgICh0aHJvdyAnYmFkISB2YXIpKSkpKSkpCj4gK8KgIChhbGlhcyBvcGVuc210cGQtbG9j YWwtZGVsaXZlcnktYWxpYXMKPiArwqDCoMKgwqDCoMKgwqDCoCAoZGVmYXVsdCAjZikKPiArwqDC oMKgwqDCoMKgwqDCoCAoc2FuaXRpemUgKGxhbWJkYSAodmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChteS9zYW5pdGl6ZSB2YXIgIm9wZW5zbXRwZC1sb2Nh bC1kZWxpdmVyeSIKPiAiYWxpYXMiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCBmYWxzZT8gb3BlbnNtdHBk LXRhYmxlPykpKSkpCj4gK8KgICh0dGwgb3BlbnNtdHBkLWxvY2FsLWRlbGl2ZXJ5LXR0bAo+ICvC oMKgwqDCoMKgwqAgKGRlZmF1bHQgI2YpCj4gK8KgwqDCoMKgwqDCoCAoc2FuaXRpemUgKGxhbWJk YSAodmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG15L3Nhbml0 aXplIHZhciAib3BlbnNtdHBkLWxvY2FsLWRlbGl2ZXJ5IiAidHRsIgo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCBm YWxzZT8gc3RyaW5nPykpKSkpCj4gK8KgICh1c2VyIG9wZW5zbXRwZC1sb2NhbC1kZWxpdmVyeS11 c2VyCj4gK8KgwqDCoMKgwqDCoMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqDCoCAoc2Fu aXRpemUgKGxhbWJkYSAodmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCAobXkvc2FuaXRpemUgdmFyICJvcGVuc210cGQtbG9jYWwtZGVsaXZlcnkiCj4gInVzZXIi Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgKGxpc3QgZmFsc2U/IHN0cmluZz8pKSkpKQo+ICvCoCAodXNlcmJhc2Ugb3Bl bnNtdHBkLWxvY2FsLWRlbGl2ZXJ5LXVzZXJiYXNlCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAg KGRlZmF1bHQgI2YpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHNhbml0aXplIChsYW1iZGEg KHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAo bXkvc2FuaXRpemUgdmFyICJvcGVuc210cGQtbG9jYWwtZGVsaXZlcnkiCj4gInVzZXJiYXNlIgo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgKGxpc3QgZmFsc2U/IG9wZW5zbXRwZC0KPiB0YWJsZT8pKSkpKQo+ ICvCoCAodmlydHVhbCBvcGVuc210cGQtbG9jYWwtZGVsaXZlcnktdmlydHVhbAo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoCAoZGVmYXVsdCAjZikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHNhbml0 aXplIChsYW1iZGEgKHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqAgKG15L3Nhbml0aXplIHZhciAib3BlbnNtdHBkLWxvY2FsLWRlbGl2ZXJ5Igo+ICJ2 aXJ0dWFsIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsaXN0IGZhbHNlPyBvcGVuc210cGQtCj4gdGFibGU/ KSkpKSkKPiArwqAgKHdyYXBwZXIgb3BlbnNtdHBkLWxvY2FsLWRlbGl2ZXJ5LXdyYXBwZXIKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqAgKGRlZmF1bHQgI2YpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg IChzYW5pdGl6ZSAobGFtYmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgIChteS9zYW5pdGl6ZSB2YXIgIm9wZW5zbXRwZC1sb2NhbC1kZWxpdmVy eSIKPiAid3JhcHBlciIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCBmYWxzZT8gc3RyaW5nPykpKSkp KQo+ICsKPiArOzsgRklYTUUvVE9ETyB0aGlzIGlzIGEgdmFsaWQgb3BlbnNtdHBkLXJlbGF5IHJl Y29yZAo+ICs7OyAob3BlbnNtdHBkLXJlbGF5Cj4gKzs7wqAgKHBraSAob3BlbnNtdHBkLXBraQo+ ICs7O8KgwqDCoMKgwqDCoMKgIChkb21haW4gImdudWNvZGUubWUiKQo+ICs7O8KgwqDCoMKgwqDC oMKgIChjZXJ0ICJvcGVuc210cGQuc2NtIikKPiArOzvCoMKgwqDCoMKgwqDCoCAoa2V5ICJvcGVu c210cGQuc2NtIikpKSkKPiArOzsgQlVUIGhvdyBkb2VzIGl0IHJlbGF5IHRoZSBlbWFpbD/CoCBX aGF0IGhvc3QgZG9lcyBpdCB1c2U/Cj4gKzs7IEkgdGhpbmsgb3BlbnNtdHBkLXJlbGF5LWNvbmZp Z3VyYXRpb24gbmVlZHMgIm1ldGhvZCIgZmllbGQuCj4gKzs7IHRoZSBtZXRob2QgZmllbGQgbWln aHQgbmVlZCB0byBiZSBhbm90aGVyIHJlY29yZC4uLkJVVCBiYXNpY2FsbHkKPiB0aGUgcmVsYXkg aGFzIHRvIGhhdmUgYSAnYmFja3VwJywgJ2JhY2t1cC1teCcsCj4gKzs7IG9yICdkb21haW4nLCBv ciAnaG9zdCcgZGVmaW5lZC4KPiArKGRlZmluZS1yZWNvcmQtdHlwZSogPG9wZW5zbXRwZC1yZWxh eT4KPiArwqAgb3BlbnNtdHBkLXJlbGF5IG1ha2Utb3BlbnNtdHBkLXJlbGF5Cj4gK8KgIG9wZW5z bXRwZC1yZWxheT8KPiArwqAgKG5hbWUgb3BlbnNtdHBkLXJlbGF5LW5hbWUKPiArwqDCoMKgwqDC oMKgwqAgKHNhbml0aXplIChsYW1iZGEgKHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgKG15L3Nhbml0aXplIHZhciAib3BlbnNtdHBkLXJlbGF5IiAibmFtZSIK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAobGlzdCBzdHJpbmc/KSkpKQo+ICvCoMKgwqDCoMKgwqDCoCAoZGVmYXVsdCAj ZikpCj4gK8KgIChiYWNrdXAgb3BlbnNtdHBkLXJlbGF5LWJhY2t1cCA7OyBib29sZWFuCj4gK8Kg wqDCoMKgwqDCoMKgwqDCoCAoZGVmYXVsdCAjZikKPiArwqDCoMKgwqDCoMKgwqDCoMKgIChzYW5p dGl6ZSAobGFtYmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAobXkvc2FuaXRpemUgdmFyICJvcGVuc210cGQtcmVsYXkiICJiYWNrdXAiCj4gK8Kg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgIChsaXN0IGJvb2xlYW4/KSkpKSkKPiArwqAgKGJhY2t1cC1teCBvcGVuc210cGQt cmVsYXktYmFja3VwLW14IDs7IHN0cmluZyBteCBuYW1lCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCAoZGVmYXVsdCAjZikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAo bGFtYmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAobXkvc2FuaXRpemUgdmFyICJvcGVuc210cGQtcmVsYXkiICJiYWNrdXAtCj4gbXgi Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsaXN0IGZhbHNlPyBzdHJpbmc/KSkpKSkKPiArwqAgKGhl bG8gb3BlbnNtdHBkLXJlbGF5LWhlbG8KPiArwqDCoMKgwqDCoMKgwqAgKHNhbml0aXplIChsYW1i ZGEgKHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG15L3Nh bml0aXplIHZhciAib3BlbnNtdHBkLXJlbGF5IiAiaGVsbyIKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCBmYWxz ZT8gc3RyaW5nPyBvcGVuc210cGQtCj4gdGFibGU/KSkpKQo+ICvCoMKgwqDCoMKgwqDCoCAoZGVm YXVsdCAjZikpCj4gK8KgIChoZWxvLXNyYyBvcGVuc210cGQtcmVsYXktaGVsby1zcmMKPiArwqDC oMKgwqDCoMKgwqAgKHNhbml0aXplIChsYW1iZGEgKHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChteS9zYW5pdGl6ZSB2YXIgIm9wZW5zbXRwZC1yZWxh eSIgImhlbG8tc3JjIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCBmYWxzZT8gc3RyaW5nPyBvcGVuc210 cGQtCj4gdGFibGU/KSkpKQo+ICvCoMKgwqDCoMKgwqDCoCAoZGVmYXVsdCAjZikpCj4gK8KgIChk b21haW4gb3BlbnNtdHBkLXJlbGF5LWRvbWFpbgo+ICvCoMKgwqDCoMKgwqDCoMKgwqAgKHNhbml0 aXplIChsYW1iZGEgKHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgIChteS9zYW5pdGl6ZSB2YXIgIm9wZW5zbXRwZC1yZWxheSIgImRvbWFpbiIKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqAgKGxpc3QgZmFsc2U/IG9wZW5zbXRwZC10YWJsZT8pKSkpCj4gK8KgwqDCoMKgwqDC oMKgwqDCoCAoZGVmYXVsdCAjZikpCj4gK8KgIChob3N0IG9wZW5zbXRwZC1yZWxheS1ob3N0Cj4g K8KgwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFtYmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChteS9zYW5pdGl6ZSB2YXIgIm9wZW5zbXRwZC1yZWxh eSIgImhvc3QiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3QgZmFsc2U/IHN0cmluZz8pKSkpCj4gK8KgwqDCoMKg wqDCoMKgIChkZWZhdWx0ICNmKSkKPiArwqAgKHBraSBvcGVuc210cGQtcmVsYXktcGtpCj4gK8Kg wqDCoMKgwqDCoCAoZGVmYXVsdCAjZikKPiArwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFtYmRh ICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2FuaXRp emUgdmFyICJvcGVuc210cGQtcmVsYXkiICJwa2kiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsaXN0IGZhbHNlPyBvcGVu c210cGQtcGtpPykpKSkpCj4gK8KgIChzcnMgb3BlbnNtdHBkLXJlbGF5LXNycwo+ICvCoMKgwqDC oMKgwqAgKGRlZmF1bHQgI2YpCj4gK8KgwqDCoMKgwqDCoCAobGFtYmRhICh2YXIpCj4gK8KgwqDC oMKgwqDCoMKgwqAgKG15L3Nhbml0aXplIHZhciAib3BlbnNtdHBkLXJlbGF5IiAic3JzIgo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3QgYm9vbGVhbj8p KSkpCj4gK8KgICh0bHMgb3BlbnNtdHBkLXJlbGF5LXRscwo+ICvCoMKgwqDCoMKgwqAgKGRlZmF1 bHQgI2YpCj4gK8KgwqDCoMKgwqDCoCAoc2FuaXRpemUgKGxhbWJkYSAodmFyKQo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG15L3Nhbml0aXplIHZhciAib3BlbnNtdHBk LXJlbGF5IiAidGxzIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCBmYWxzZT8gc3RyaW5nPykpKSkpCj4gK8KgIChh dXRoIG9wZW5zbXRwZC1yZWxheS1hdXRoCj4gK8KgwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFt YmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChteS9z YW5pdGl6ZSB2YXIgIm9wZW5zbXRwZC1yZWxheSIgImF1dGgiCj4gK8KgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3QgZmFs c2U/IG9wZW5zbXRwZC10YWJsZT8pKSkpCj4gK8KgwqDCoMKgwqDCoMKgIChkZWZhdWx0ICNmKSkK PiArwqAgKG1haWwtZnJvbSBvcGVuc210cGQtcmVsYXktbWFpbC1mcm9tCj4gK8KgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoCAoZGVmYXVsdCAjZikpCj4gK8KgIDs7IHN0cmluZyAiMTI3LjAuMC4xIiBv ciAiPGludGVyZmFjZT4iIG9yICI8dGFibGUgb2YgSVAKPiBhZGRyZXNzZXM+Igo+ICvCoCA7OyBU T0RPIHNob3VsZCBJIGRvIHNvbWUgc2FuaXRpemluZyB0byBtYWtlIHN1cmUgdGhhdCB0aGUgc3Ry aW5nPwo+IGhlcmUgaXMgYWN0dWFsbHkgYW4gSVAgYWRkcmVzcyBvciBhIHZhbGlkIGludGVyZmFj ZT8KPiArwqAgKHNyYyBvcGVuc210cGQtcmVsYXktc3JjCj4gK8KgwqDCoMKgwqDCoCAoc2FuaXRp emUgKGxhbWJkYSAodmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAg KG15L3Nhbml0aXplIHZhciAib3BlbnNtdHBkLXJlbGF5IiAic3JjIgo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCBm YWxzZT8gc3RyaW5nPyBvcGVuc210cGQtCj4gdGFibGU/KSkpKQo+ICvCoMKgwqDCoMKgwqAgKGRl ZmF1bHQgI2YpKSkKPiArCj4gKzs7IHRoaXMgcmVjb3JkIGlzIHVzZWQgYnkgPG9wZW5zbXRwZC1m aWx0ZXItcGhhc2U+ICYKPiArOzsgPG9wZW5zbXRwZC1tYXRjaD4KPiArKGRlZmluZS1yZWNvcmQt dHlwZSogPG9wZW5zbXRwZC1vcHRpb24+Cj4gK8KgIG9wZW5zbXRwZC1vcHRpb24gbWFrZS1vcGVu c210cGQtb3B0aW9uCj4gK8KgIG9wZW5zbXRwZC1vcHRpb24/Cj4gK8KgIChvcHRpb24gb3BlbnNt dHBkLW9wdGlvbi1vcHRpb24KPiArwqDCoMKgwqDCoMKgwqDCoMKgIChkZWZhdWx0ICNmKQo+ICvC oMKgwqDCoMKgwqDCoMKgwqAgKHNhbml0aXplIChsYW1iZGEgKHZhcikKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChpZiAoYW5kIChzdHJpbmc/IHZhcikKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIChvciAoc3RyaW5nLWluLWxpc3Q/IHZhciAobGlzdAo+ICJmY3JkbnMiICJyZG5zIgo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAg InNyYyIKPiAiaGVsbyIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgICJhdXRoIgo+ICJtYWlsLWZyb20iCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAicmNwdC10byIKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg ICJmb3IiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAiZm9yIGFueSIKPiAiZm9yIGxvY2FsIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgImZvcgo+IGRvbWFpbiIgImZv ciByY3B0LXRvIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgImZyb20gYW55Igo+ICJmcm9tIGF1dGgiCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAiZnJvbQo+IGxvY2Fs IiAiZnJvbSBtYWlsLWZyb20iCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAiZnJvbQo+IHJkbnMiICJmcm9tIHNvY2tldCIKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJm cm9tIHNyYyIKPiAiYXV0aCIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgICJoZWxvIgo+ICJtYWlsLWZyb20iCj4gK8KgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAicmNwdC10byIK PiAidGFnIiAidGxzIikpKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgIAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoCB2YXIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqAgKGJlZ2luCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZGlzcGxheSAoc3RyaW5nLWFwcGVuZCAiPG9w ZW5zbXRwZC0KPiBvcHRpb24+IGZpZWxkbmFtZTogJ29wdGlvbicgaXMgb2YgdHlwZSBcbiIKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJzdHJpbmcuwqAg VGhlCj4gc3RyaW5nIGNhbiBiZSBlaXRoZXIgJ2ZjcmRucycsIFxuIgo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIiAncmRucycsICdzcmMnLAo+ICdoZWxv JywgJ2F1dGgnLCAnbWFpbC1mcm9tJywgb3IgJ3JjcHQtdG8nLCBcbiIKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICInZm9yJywgJ2Zvcgo+IGFueScsICdm b3IgbG9jYWwnLCAnZm9yIGRvbWFpbicsICdmb3IgcmNwdC10bycsIFxuIgo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIidmcm9tIGFueScsCj4gJ2Zyb20g YXV0aCcsICdmcm9tIGxvY2FsJywgJ2Zyb20gbWFpbC1mcm9tJywgJ2Zyb20gcmRucycsICdmcm9t Cj4gc29ja2V0JywgXG4iCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAiJ2Zyb20gc3JjJywKPiAnYXV0aCBoZWxvJywgJ21haWwtZnJvbScsICdyY3B0LXRv JywgJ3RhZycsIG9yICd0bHMnIFxuIikpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoCAKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgICh0aHJvdyAnYmFkISB2YXIpKSkpKSkKPiArwqAgKG5vdCBvcGVu c210cGQtb3B0aW9uLW5vdAo+ICvCoMKgwqDCoMKgwqAgKGRlZmF1bHQgI2YpCj4gK8KgwqDCoMKg wqDCoCAoc2FuaXRpemUgKGxhbWJkYSAodmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgKG15L3Nhbml0aXplIHZhciAib3BlbnNtdHBkLW9wdGlvbiIgIm5vdCIKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKGxpc3QgYm9vbGVhbj8pKSkpKQo+ICvCoCAocmVnZXggb3BlbnNtdHBkLW9wdGlvbi1y ZWdleAo+ICvCoMKgwqDCoMKgwqDCoMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqDCoMKg IChzYW5pdGl6ZSAobGFtYmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgKG15L3Nhbml0aXplIHZhciAib3BlbnNtdHBkLW9wdGlvbiIgInJlZ2V4Igo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgKGxpc3QgYm9vbGVhbj8pKSkpKQo+ICvCoCAoZGF0YSBvcGVuc210cGQtb3B0 aW9uLWRhdGEKPiArwqDCoMKgwqDCoMKgwqDCoCAoZGVmYXVsdCAjZikKPiArwqDCoMKgwqDCoMKg wqDCoCAoc2FuaXRpemUgKGxhbWJkYSAodmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgIChteS9zYW5pdGl6ZSB2YXIgIm9wZW5zbXRwZC1vcHRpb24iICJkYXRh Igo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgKGxpc3QgZmFsc2U/IHN0cmluZz8gb3BlbnNtdHBkLQo+IHRhYmxlPykp KSkpKQo+ICsKPiArKGRlZmluZS1yZWNvcmQtdHlwZSogPG9wZW5zbXRwZC1maWx0ZXItcGhhc2U+ Cj4gK8KgIG9wZW5zbXRwZC1maWx0ZXItcGhhc2UgbWFrZS1vcGVuc210cGQtZmlsdGVyLXBoYXNl Cj4gK8KgIG9wZW5zbXRwZC1maWx0ZXItcGhhc2U/Cj4gK8KgIChuYW1lIG9wZW5zbXRwZC1maWx0 ZXItcGhhc2UtbmFtZSA7OyBzdHJpbmcgY2hhaW4tbmFtZQo+ICvCoMKgwqDCoMKgwqDCoCAoZGVm YXVsdCAjZikKPiArwqDCoMKgwqDCoMKgwqAgKHNhbml0aXplIChsYW1iZGEgKHZhcikKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG15L3Nhbml0aXplIHZhciAib3Bl bnNtdHBkLWZpbHRlci1waGFzZSIgIm5hbWUiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3Qgc3RyaW5nPykpKSkp Cj4gK8KgIChwaGFzZSBvcGVuc210cGQtZmlsdGVyLXBoYXNlLXBoYXNlIDs7IHN0cmluZwo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZGVmYXVsdCAjZikKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgKHNhbml0aXplIChsYW1iZGEgKHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGlmIChhbmQgKHN0cmluZz8gdmFyKQo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoCAoc3RyaW5nLWluLWxpc3Q/IHZhciAobGlzdAo+ICJjb25uZWN0Igo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAiaGVsbyIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIm1haWwtCj4gZnJvbSIKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgInJj cHQtCj4gdG8iCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgICJkYXRhIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoAo+ICJjb21taXQiKSkpCj4g K8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgdmFyCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgKGJlZ2luCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChkaXNwbGF5IChzdHJpbmctYXBwZW5kICI8b3Bl bnNtdHBkLQo+IGZpbHRlci1waGFzZT4gZmllbGRuYW1lOiAncGhhc2UnIGlzIG9mIHR5cGUgXG4i Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg ICJzdHJpbmcuwqAKPiBUaGUgc3RyaW5nIGNhbiBiZSBlaXRoZXIgJ2Nvbm5lY3QnLCIKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIiAnaGVs bycsCj4gJ21haWwtZnJvbScsICdyY3B0LXRvJywgJ2RhdGEnLCBvciAnY29tbWl0LidcbiAiKSkK PiArCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgICh0aHJvdyAnYmFkISB2YXIpKSkpKSkKPiArwqAgKG9wdGlvbnMgb3BlbnNt dHBkLWZpbHRlci1waGFzZS1vcHRpb25zCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgIChkZWZhdWx0 ICNmKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc2FuaXRpemUgKGxhbWJkYSAodmFyKQo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCA7OyByZXR1cm5zICN0 IGlmIGxpc3QgaXMgYSB1bmlxdWUgbGlzdCBvZgo+IDxvcGVuc210cGQtb3B0aW9uPgo+ICvCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZGVmaW5lIChsaXN0LW9m LW9wZW5zbXRwZC1vcHRpb24/IGxpc3QpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoCAoYW5kIChsaXN0LW9mLXR5cGU/IGxpc3Qgb3BlbnNtdHBkLW9w dGlvbj8pCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgKG5vdCAoY29udGFpbnMtZHVwbGljYXRlPyBsaXN0KSkpKQo+ICsKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGRlZmluZSAobGlzdC1o YXMtZHVwbGljYXRlcy1vci1ub24tCj4gb3BlbnNtdHBkLW9wdGlvbiBsaXN0KQo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG5vdCAobGlzdC1vZi1v cGVuc210cGQtb3B0aW9uPyBsaXN0KSkpCj4gKwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoCA7OyBpbnB1dCA8b3BlbnNtdHBkLW9wdGlvbj4KPiArwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgOzsgcmV0dXJuICN0IGlmIDxv cGVuc210cGQtb3B0aW9uPiBmaWVsZG5hbWUKPiAnb3B0aW9uJwo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCA7OyB0aGF0IG5lZWRzIGEgY29ycmVzcG9uZGlu ZyB0YWJsZSBoYXMgb25lLsKgCj4gT3RoZXJ3aXNlICNmCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChkZWZpbmUgKG9wZW5zbXRwZC1vcHRpb24taGFzLXRh YmxlPyByZWNvcmQpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAoZGVmaW5lIGRlY2lzaW9uIChvcGVuc210cGQtb3B0aW9uLW9wdGlvbgo+IHJlY29y ZCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAo YW5kIChzdHJpbmc/IGRlY2lzaW9uKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIDs7IGlmIG9wdGlvbiBuZWVkcyBhIHRhYmxlLCBj aGVjayBmb3IKPiBhIHRhYmxlCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGlmIChzdHJpbmctaW4tbGlzdD8gZGVjaXNpb24gKGxp c3QKPiAic3JjIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgCj4gImhlbG8iCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAKPiAibWFpbC1mcm9tIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgCj4gInJjcHQtdG8iKSkKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgIChvcGVuc210cGQtdGFibGU/IChvcGVuc210cGQtCj4gb3B0aW9uLWRhdGEgcmVjb3Jk KSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgICN0KSkpCj4gKwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoCAoZGVmaW5lIChsaXN0LW9mLW9wZW5zbXRwZC1vcHRpb24taGFzLXRh YmxlPwo+IGxpc3QpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAobGlzdC1vZi10eXBlPyBsaXN0IG9wZW5zbXRwZC1vcHRpb24taGFzLQo+IHRhYmxl PykpCj4gKwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAo ZGVmaW5lIChzb21lLW9wZW5zbXRwZC1vcHRpb24taW4tbGlzdC1sYWNrLQo+IHRhYmxlPyBsaXN0 KQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG5v dCAobGlzdC1vZi1vcGVuc210cGQtb3B0aW9uLWhhcy10YWJsZT8KPiBsaXN0KSkpCj4gKwo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc2FuaXRpemUtb3B0 aW9ucy1mb3ItZmlsdGVyLXBoYXNlLQo+IGNvbmZpZ3VyYXRpb24gdmFyKQo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCApKSkKPiArwqAgKGRlY2lzaW9uIG9w ZW5zbXRwZC1maWx0ZXItcGhhc2UtZGVjaXNpb24KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAo ZGVmYXVsdCAjZikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc2FuaXRpemUgKGxhbWJkYSAo dmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChp ZiAoYW5kIChzdHJpbmc/IHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc3RyaW5nLWluLWxpc3Q/IHZhciAobGlz dCAiYnlwYXNzIgo+ICJkaXNjb25uZWN0Igo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJyZWplY3QiCj4gInJld3JpdGUiICJq dW5rIikpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqAgdmFyCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoCAoYmVnaW4KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZGlzcGxheSAoc3RyaW5nLWFwcGVuZCAiPG9wZW5z bXRwZC0KPiBmaWx0ZXItZGVjaXNpb24+IGZpZWxkbmFtZTogJ2RlY2lzaW9uJyBpcyBvZiB0eXBl IFxuIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg ICJzdHJpbmcuwqAgVGhlCj4gc3RyaW5nIGNhbiBiZSBlaXRoZXIgJ2J5cGFzcycsIgo+ICvCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICIKPiAnZGlzY29u bmVjdCcsICdyZWplY3QnLCAncmV3cml0ZScsIG9yICdqdW5rJy5cbiIpKQo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICh0aHJvdyAn YmFkISB2YXIpKSkpKSkKPiArwqAgKG1lc3NhZ2Ugb3BlbnNtdHBkLWZpbHRlci1waGFzZS1tZXNz YWdlCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoCAoc2FuaXRpemUgKGxhbWJkYSAodmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2FuaXRpemUgdmFyICJvcGVuc210cGQtZmlsdGVy LXBoYXNlIgo+ICJtZXNzYWdlIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsaXN0IGZhbHNlPyBzdHJpbmc/ KSkpKSkKPiArwqAgKHZhbHVlIG9wZW5zbXRwZC1maWx0ZXItcGhhc2UtdmFsdWUKPiArwqDCoMKg wqDCoMKgwqDCoCAoZGVmYXVsdCAjZikKPiArwqDCoMKgwqDCoMKgwqDCoCAoc2FuaXRpemUgKGxh bWJkYSAodmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICht eS9zYW5pdGl6ZSB2YXIgIm9wZW5zbXRwZC1maWx0ZXItcGhhc2UiCj4gInZhbHVlIgo+ICvCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKGxpc3QgZmFsc2U/IG51bWJlcj8pKSkpKSkKPiArCj4gKyhkZWZpbmUtcmVjb3JkLXR5 cGUqIDxvcGVuc210cGQtZmlsdGVyPgo+ICvCoCBvcGVuc210cGQtZmlsdGVyIG1ha2Utb3BlbnNt dHBkLWZpbHRlcgo+ICvCoCBvcGVuc210cGQtZmlsdGVyPwo+ICvCoCAobmFtZSBvcGVuc210cGQt ZmlsdGVyLW5hbWUKPiArwqDCoMKgwqDCoMKgwqAgKGRlZmF1bHQgI2YpCj4gK8KgwqDCoMKgwqDC oMKgIChzYW5pdGl6ZSAobGFtYmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIChteS9zYW5pdGl6ZSB2YXIgIm9wZW5zbXRwZC1maWx0ZXIiICJuYW1lIgo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgIChsaXN0IHN0cmluZz8pKSkpKQo+ICvCoCAoZXhlYyBvcGVuc210cGQtZmlsdGVy LWV4ZWMKPiArwqDCoMKgwqDCoMKgwqAgKGRlZmF1bHQgI2YpCj4gK8KgwqDCoMKgwqDCoMKgIChz YW5pdGl6ZSAobGFtYmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgIChteS9zYW5pdGl6ZSB2YXIgIm9wZW5zbXRwZC1maWx0ZXIiICJleGVjIgo+ICvCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIChsaXN0IGJvb2xlYW4/KSkpKSkKPiArwqAgKHByb2Mgb3BlbnNtdHBkLWZpbHRlci1wcm9j IDsgYSBzdHJpbmcgbGlrZSAicnNwYW1kIiBvciB0aGUKPiBjb21tYW5kIHRvIHN0YXJ0IGl0IGxp a2UgIi9wYXRoL3RvL3JzcGFtZCAtLW9wdGlvbj1hcmcgLS0ybmQtCj4gb3B0aW9uPWFyZzIiCj4g K8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZGVmYXVsdCAjZikKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgIChzYW5pdGl6ZSAobGFtYmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2FuaXRpemUgdmFyICJvcGVuc210cGQt ZmlsdGVyIiAicHJvYyIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3Qgc3RyaW5nPyBsaXN0LW9m LXN0cmluZ3MtCj4gb3ItZ2V4cHM/KSkpKSkpCj4gKwo+ICs7OyBUaGVyZSBpcyBhbm90aGVyIHR5 cGUgb2YgZmlsdGVyIHRoYXQgb3BlbnNtdHBkIHN1cHBvcnRzLCB3aGljaCBpcwo+IGEgZmlsdGVy IGNoYWluLgo+ICs7OyBBIGZpbHRlciBjaGFpbiBpcyBhIGxpc3Qgb2YgPG9wZW5zbXRwZC1maWx0 ZXItcGhhc2U+IGFuZAo+IDxvcGVuc210cGQtZmlsdGVyPi4KPiArOzsgVGhpcyBsZXRzIHlvdSBh cHBseSBzZXZlcmFsIGZpbHRlcnMgdW5kZXIgb25lIGZpbHRlciBuYW1lLsKgIEkKPiBjb3VsZCBo YXZlIGRlZmluZWQKPiArOzsgYSByZWNvcmQgdHlwZSBmb3IgaXQsIGJ1dCB0aGUgcmVjb3JkIHdv dWxkIG9ubHkgaGF2ZSBoYWQgdHdvCj4gZmllbGRzOiBuYW1lIGFuZCBsaXN0LW9mLWZpbHRlcnMu Cj4gKzs7IFdoeSB3cml0ZSB0aGF0IGFzIGEgcmVjb3JkP8KgIFRoYXQncyB0b28gc2ltcGxlLgo+ ICs7OyByZXR1cm5zICN0IGlmIGxpc3QgaXMgYSB1bmlxdWUgbGlzdCBvZiA8b3BlbnNtdHBkLWZp bHRlcj4gb3IKPiA8b3BlbnNtdHBkLWZpbHRlci1waGFzZT4KPiArOzsgcmV0dXJucyAjIG90aGVy d2lzZQo+ICsoZGVmaW5lIChvcGVuc210cGQtZmlsdGVyLWNoYWluPyAlZmlsdGVycykKPiArwqAg KGFuZCAobGlzdC1vZi11bmlxdWUtZmlsdGVyLW9yLWZpbHRlci1waGFzZT8gJWZpbHRlcnMpCj4g K8KgwqDCoMKgwqDCoCAoPCAxIChsZW5ndGggJWZpbHRlcnMpKSkpCj4gKwo+ICsoZGVmaW5lLXJl Y29yZC10eXBlKiA8b3BlbnNtdHBkLWludGVyZmFjZT4KPiArwqAgb3BlbnNtdHBkLWludGVyZmFj ZSBtYWtlLW9wZW5zbXRwZC1pbnRlcmZhY2UKPiArwqAgb3BlbnNtdHBkLWludGVyZmFjZT8KPiAr wqAgOzsgaW50ZXJmYWNlIG1heSBiZSBhbiBJUCBhZGRyZXNzLCBpbnRlcmZhY2UgZ3JvdXAsIG9y IGRvbWFpbiBuYW1lCj4gK8KgIChpbnRlcmZhY2Ugb3BlbnNtdHBkLWludGVyZmFjZS1pbnRlcmZh Y2UKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChkZWZhdWx0ICJsbyIpKQo+ICvCoCAoZmFt aWx5IG9wZW5zbXRwZC1pbnRlcmZhY2UtZmFtaWx5Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoCAoZGVm YXVsdCAjZikKPiArwqDCoMKgwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFtYmRhICh2YXIpCj4g K8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoY29uZAo+ICvCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoKGVxPyAjZiB2YXIpIDs7 IHZhciA9PSAjZgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAg KChhbmQgKHN0cmluZz8gdmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc3RyaW5nLWluLWxpc3Q/IHZhciAobGlzdCAiaW5ldDQi Cj4gImluZXQ2IikpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgIHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgKGVsc2UKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCAoYmVnaW4KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKGRpc3BsYXkgIjxvcGVuc210cGQtaW50ZXJmYWNlPiBmaWVsZG5hbWUKPiAnZmFtaWx5 JyBtdXN0IGJlIHN0cmluZyBcImluZXQ0XCIgb3IgXCJpbmV0NlwiLlxuIikKPiArwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHRocm93ICdiYWQhIHZh cikpKSkpKSkKPiArwqAgKGF1dGggb3BlbnNtdHBkLWludGVyZmFjZS1hdXRoCj4gK8KgwqDCoMKg wqDCoMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqDCoCAoc2FuaXRpemUgKGxhbWJkYSAo dmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2FuaXRp emUgdmFyICJvcGVuc210cGQtaW50ZXJmYWNlIiAiYXV0aCIKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCBib29s ZWFuPyB0YWJsZS13aG9zZS1kYXRhLQo+IGFyZS1hc3NvYy1saXN0PykpKSkpCj4gK8KgIChhdXRo LW9wdGlvbmFsIG9wZW5zbXRwZC1pbnRlcmZhY2UtYXV0aC1vcHRpb25hbAo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZGVmYXVsdCAjZikKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgKHNhbml0aXplIChsYW1iZGEgKHZhcikKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG15L3Nhbml0aXplIHZh ciAib3BlbnNtdHBkLWludGVyZmFjZSIKPiAiYXV0aC1vcHRpb25hbCIKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoCAobGlzdCBib29sZWFuPwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIHRhYmxlLXdob3NlLWRhdGEtCj4gYXJlLWFzc29jLWxpc3Q/KSkpKSkKPiAr wqAgOzsgVE9ETyBhZGQgYSBjYSBlbnRyeT8KPiArwqAgOzsgc3RyaW5nIEZJWE1FL1RPRE8gc2Fu aXRpemUgdGhpcyB0byBzdXBwb3J0IGEgZ2V4cC7CoCBUaGF0IHdheQo+IHdheSB0aGUKPiArwqAg OzsgaW5jbHVkZXMgZGlyZWN0aXZlIGNhbiBpbmNsdWRlIG15IGhhY2t5IHNjaGVtZSBjb2RlIHRo YXQgSSB1c2UKPiBmb3Igb3BlbnNtdHBkLWRraW1zaWduLgo+ICvCoCAoZmlsdGVycyBvcGVuc210 cGQtaW50ZXJmYWNlLWZpbHRlcnMKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGRlZmF1bHQgI2Yp Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFtYmRhICh2YXIpCj4gK8KgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzYW5pdGl6ZS1maWx0ZXIt cGhhc2VzIHZhcikpKSkKPiArwqAgKGhvc3RuYW1lIG9wZW5zbXRwZC1pbnRlcmZhY2UtaG9zdG5h bWUKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZGVmYXVsdCAjZikKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAoc2FuaXRpemUgKGxhbWJkYSAodmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChteS9zYW5pdGl6ZSB2YXIgIm9wZW5zbXRwZC1p bnRlcmZhY2UiCj4gImhvc3RuYW1lIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3QgZmFsc2U/IHN0 cmluZz8pKSkpKQo+ICvCoCAoaG9zdG5hbWVzIG9wZW5zbXRwZC1pbnRlcmZhY2UtaG9zdG5hbWVz Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZGVmYXVsdCAjZikKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFtYmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2FuaXRpemUgdmFyICJvcGVuc210 cGQtaW50ZXJmYWNlIgo+ICJob3N0bmFtZXMiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsaXN0IGZh bHNlPyB0YWJsZS13aG9zZS1kYXRhLQo+IGFyZS1hc3NvYy1saXN0PykpKSkpCj4gK8KgIChtYXNr LXNyYyBvcGVuc210cGQtaW50ZXJmYWNlLW1hc2stc3JjCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqAgKGRlZmF1bHQgI2YpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHNhbml0aXplIChsYW1i ZGEgKHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCAobXkvc2FuaXRpemUgdmFyICJvcGVuc210cGQtaW50ZXJmYWNlIgo+ICJtYXNrLXNyYyIKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgIChsaXN0IGJvb2xlYW4/KSkpKSkKPiArwqAgKGRpc2FibGUtZHNuIG9w ZW5zbXRwZC1pbnRlcmZhY2UtZGlzYWJsZS1kc24KPiArwqDCoMKgwqDCoMKgwqDCoMKgIChkZWZh dWx0ICNmKSkKPiArwqAgKHBraSBvcGVuc210cGQtaW50ZXJmYWNlLXBraQo+ICvCoMKgwqDCoMKg wqAgKGRlZmF1bHQgI2YpCj4gK8KgwqDCoMKgwqDCoCAoc2FuaXRpemUgKGxhbWJkYSAodmFyKQo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG15L3Nhbml0aXplIHZhciAi b3BlbnNtdHBkLWludGVyZmFjZSIgInBraSIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3QgZmFsc2U/IG9wZW5zbXRw ZC1wa2k/KSkpKSkKPiArwqAgKHBvcnQgb3BlbnNtdHBkLWludGVyZmFjZS1wb3J0Cj4gK8KgwqDC oMKgwqDCoMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqDCoCAoc2FuaXRpemUgKGxhbWJk YSAodmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2Fu aXRpemUgdmFyICJvcGVuc210cGQtaW50ZXJmYWNlIiAicG9ydCIKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCBm YWxzZT8gaW50ZWdlcj8pKSkpKQo+ICvCoCAocHJveHktdjIgb3BlbnNtdHBkLWludGVyZmFjZS1w cm94eS1rMgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChkZWZhdWx0ICNmKSkKPiArwqAgKHJl Y2VpdmVkLWF1dGggb3BlbnNtdHBkLWludGVyZmFjZS1yZWNlaXZlZC1hdXRoCj4gK8KgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChkZWZhdWx0ICNmKSkKPiArwqAgOzsgVE9ETyBhZGQg aW4gYSBzZW5kZXJzIG9wdGlvbiEKPiArwqAgOzsgc3RyaW5nIG9yIDxvcGVuc210cGQtc2VuZGVy cz4gcmVjb3JkCj4gK8KgIDs7IChzZW5kZXJzIG9wZW5zbXRwZC1pbnRlcmZhY2Utc2VuZGVycwo+ ICvCoCA7O8KgwqDCoMKgwqDCoMKgwqDCoCAoc2FuaXRpemUgKGxhbWJkYSAodmFyKQo+ICvCoCA7 O8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2FuaXRpemUg dmFyICJvcGVuc210cGQtaW50ZXJmYWNlIgo+ICJwb3J0IiAobGlzdCBmYWxzZT8gaW50ZWdlcj8p KSkpCj4gK8KgIDs7wqDCoMKgwqDCoMKgwqDCoMKgIChkZWZhdWx0ICNmKSkKPiArwqAgKHNlY3Vy ZS1jb25uZWN0aW9uIG9wZW5zbXRwZC1pbnRlcmZhY2Utc2VjdXJlLWNvbm5lY3Rpb24KPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZGVmYXVsdCAjZikKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc2FuaXRpemUgKGxhbWJkYSAo dmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgIChjb25kICgoYm9vbGVhbj8gdmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgdmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICgoYW5kIChzdHJpbmc/IHZhcikKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc3RyaW5nLWluLWxpc3Q/IHZhcgo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAobGlzdAo+ICJzbXRwcyIgInRscyIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAKPiAi dGxzLXJlcXVpcmUiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgCj4gInRscy1yZXF1aXJlLXZlcmlm eSIpKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCAoZWxzZQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGJlZ2luCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgKGRpc3BsYXkgKHN0cmluZy1hcHBlbmQKPiAiPG9wZW5zbXRkLWxpc3Rlbi1v bj4gZmllbGRuYW1lICdzZWN1cmUtY29ubmVjdGlvbicgY2FuIGJlIFxuIgo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgCj4gIm9uZSBvZiB0aGUgZm9sbG93aW5nIHN0cmluZ3M6IFxuJ3NtdHBzJywgJ3Rscycs ICd0bHMtcmVxdWlyZScsIFxuIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgCj4gIm9yICd0bHMtcmVxdWly ZS12ZXJpZnknLlxuIikpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHRocm93ICdiYWQh IHZhcikpKSkpKSkKWW91IG1pZ2h0IHdhbnQgdG8gcmVkdWNlIGhvcml6b250YWwgc3BhY2UgaGVy ZSwgZXZlbiBpZiBndWl4IHN0eWxlCnRlbGxzIHlvdSBvdGhlcndpc2UuCj4gK8KgICh0YWcgb3Bl bnNtdHBkLWludGVyZmFjZS10YWcKPiArwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFtYmRhICh2 YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2FuaXRpemUg dmFyICJvcGVuc210cGQtaW50ZXJmYWNlIiAidGFnIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCBmYWxzZT8gc3Ry aW5nPykpKSkKPiArwqDCoMKgwqDCoMKgIChkZWZhdWx0ICNmKSkpCj4gKwo+ICsoZGVmaW5lLXJl Y29yZC10eXBlKiA8b3BlbnNtdHBkLXNvY2tldC1jb25maWd1cmF0aW9uPgo+ICvCoCBvcGVuc210 cGQtc29ja2V0LWNvbmZpZ3VyYXRpb24gbWFrZS1vcGVuc210cGQtc29ja2V0LWNvbmZpZ3VyYXRp b24KPiArwqAgb3BlbnNtdHBkLXNvY2tldC1jb25maWd1cmF0aW9uPwo+ICvCoCA7OyBmYWxzZSBv ciA8b3BlbnNtdHBkLWZpbHRlcj4gb3IgbGlzdCBvZiA8b3BlbnNtdHBkLWZpbHRlcj4KPiArwqAg KGZpbHRlcnMgb3BlbnNtdHBkLXNvY2tldC1jb25maWd1cmF0aW9uLWZpbHRlcnMKPiArwqDCoMKg wqDCoMKgwqDCoMKgwqAgKHNhbml0aXplIChsYW1iZGEgKHZhcikKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHNhbml0aXplLWZpbHRlci1waGFzZXMgdmFy KSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoCAoZGVmYXVsdCAjZikpCj4gK8KgIChtYXNrLXNyYyBv cGVuc210cGQtc29ja2V0LWNvbmZpZ3VyYXRpb24tbWFzay1zcmMKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAoZGVmYXVsdCAjZikpCj4gK8KgICh0YWcgb3BlbnNtdHBkLXNvY2tldC1jb25maWd1 cmF0aW9uLXRhZwo+ICvCoMKgwqDCoMKgwqAgKHNhbml0aXplIChsYW1iZGEgKHZhcikKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChteS9zYW5pdGl6ZSB2YXIgIm9wZW5z bXRwZC1pbnRlcmZhY2UiICJ0YWciCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsaXN0IGZhbHNlPyBzdHJpbmc/KSkpKQo+ ICvCoMKgwqDCoMKgwqAgKGRlZmF1bHQgI2YpKSkKPiArCj4gKwo+ICsoZGVmaW5lLXJlY29yZC10 eXBlKiA8b3BlbnNtdHBkLW1hdGNoPgo+ICvCoCBvcGVuc210cGQtbWF0Y2ggbWFrZS1vcGVuc210 cGQtbWF0Y2gKPiArwqAgb3BlbnNtdHBkLW1hdGNoPwo+ICvCoCA7O1RPRE8/IFBlcmhhcHMgSSBz aG91bGQgYWRkIGluIGEgcmVqZWN0IGZpZWxkbmFtZS7CoCBJZiByZWplY3QKPiArwqAgOztpcyAj dCwgdGhlbiB0aGUgbWF0Y2ggcmVjb3JkIHdpbGwgYmUgYSByZWplY3QgbWF0Y2ggcmVjb3JkLgo+ ICvCoCA7OyAob3BlbnNtdHBkLW1hdGNoIChyZWplY3QgI3QpKSB2cy4gKG9wZW5zbXRwZC1tYXRj aCAoYWN0aW9uCj4gJ3JlamVjdCkpCj4gK8KgIDs7IFRvIGRvIHRoaXMsIEkgd2lsbCBhbHNvIGhh dmUgdG/CoCAncmVqZWN0JyBtdXR1YWxseSBleGNsdXNpdmUuCj4gQU5EIGFuIG1hdGNoIHdpdGgg J3JlamVjdCcgY2FuIGhhdmUgbm8gYWN0aW9uIGRlZmluZWQuCj4gK8KgIChhY3Rpb24gb3BlbnNt dHBkLW1hdGNoLWFjdGlvbgo+ICvCoMKgwqDCoMKgwqDCoMKgwqAgKGRlZmF1bHQgI2YpCj4gK8Kg wqDCoMKgwqDCoMKgwqDCoCAoc2FuaXRpemUgKGxhbWJkYSAodmFyKQo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGlmIChvciAob3BlbnNtdHBkLXJlbGF5PyB2 YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqAgKG9wZW5zbXRwZC1sb2NhbC1kZWxpdmVyeT8gdmFyKQo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChlcT8gKHF1b3Rl IHJlamVjdCkgdmFyKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgdmFyCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIChiZWdpbgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqAgKGRpc3BsYXkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHN0cmluZy1hcHBlbmQgIjxvcGVuc210 cGQtbWF0Y2g+Cj4gZmllbGRuYW1lICdhY3Rpb24nIGlzIG9mIHR5cGUgPG9wZW5zbXRwZC1yZWxh eT4sIFxuIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAiPG9wZW5zbXRwZC1sb2Nh bC0KPiBkZWxpdmVyeT4sIG9yIChxdW90ZSByZWplY3QpLlxuIgo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoCAiSWYgaXRzIHZhciBpcyAocXVvdGUKPiByZWplY3QpLCB0aGVuIHRoZSBt YXRjaCByZWplY3RzIHRoZSBpbmNvbWluZyBtZXNzYWdlXG4iCj4gK8KgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgICJkdXJpbmcgdGhlIFNNVFAKPiBkaWFsb2d1ZS5cbiIpKQo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHRocm93ICdi YWQhIHZhcikpKSkpKQo+ICvCoCAob3B0aW9ucyBvcGVuc210cGQtbWF0Y2gtb3B0aW9ucwo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoCAoZGVmYXVsdCAjZikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqAg KHNhbml0aXplIChsYW1iZGEgKHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgKGNvbmQgKChub3QgdmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICNmKQo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoKG5vdCAobGlzdC1v Zi11bmlxdWUtb3BlbnNtdHBkLW9wdGlvbj8KPiB2YXIpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICh0aHJvdy1lcnJvciB2YXIg JygiPG9wZW5zbXRwZC1tYXRjaD4KPiBmaWVsZG5hbWUgJ29wdGlvbnMnIGlzIGEgbGlzdCBvZiB1 bmlxdWUgXG4iCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAiPG9w ZW5zbXRwZC1vcHRpb24+Cj4gcmVjb3Jkcy4gXG4iKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGVsc2UgKHNhbml0aXplLWxpc3Qtb2Yt b3B0aW9ucy1mb3ItCj4gbWF0Y2gtY29uZmlndXJhdGlvbiB2YXIpKSkpKSkpCj4gKwo+ICsoZGVm aW5lLXJlY29yZC10eXBlKiA8b3BlbnNtdHBkLXNtdHA+Cj4gK8KgIG9wZW5zbXRwZC1zbXRwIG1h a2Utb3BlbnNtdHBkLXNtdHAKPiArwqAgb3BlbnNtdHBkLXNtdHA/Cj4gK8KgIChjaXBoZXJzIG9w ZW5zbXRwZC1zbXRwLWNpcGhlcnMKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGRlZmF1bHQgI2Yp Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFtYmRhICh2YXIpCj4gK8KgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChteS9zYW5pdGl6ZSB2YXIg Im9wZW5zbXRwZC1zbXRwIiAiY2lwaGVycyIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCBmYWxzZT8g c3RyaW5nPykpKSkpCj4gK8KgIChsaW1pdC1tYXgtbWFpbHMgb3BlbnNtdHBkLXNtdHAtbGltaXQt bWF4LW1haWxzCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZGVmYXVs dCAjZikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAo bGFtYmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2FuaXRpemUgdmFyICJvcGVuc210cGQtc210cCIKPiAi bGltaXQtbWF4LW1haWxzIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCBm YWxzZT8KPiBpbnRlZ2VyPykpKSkpCj4gK8KgIChsaW1pdC1tYXgtcmNwdCBvcGVuc210cGQtc210 cC1saW1pdC1tYXgtcmNwdAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChk ZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzYW5pdGl6 ZSAobGFtYmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqAgKG15L3Nhbml0aXplIHZhciAib3BlbnNtdHBkLXNtdHAiCj4g ImxpbWl0LW1heC1yY3B0Igo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3QgZmFs c2U/Cj4gaW50ZWdlcj8pKSkpKQo+ICvCoCAobWF4LW1lc3NhZ2Utc2l6ZSBvcGVuc210cGQtc210 cC1tYXgtbWVzc2FnZS1zaXplCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCAoc2FuaXRpemUgKGxhbWJkYSAodmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2FuaXRpemUgdmFyICJvcGVu c210cGQtc210cCIKPiAibWF4LW1lc3NhZ2Utc2l6ZSIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAobGlzdCBmYWxzZT8gaW50ZWdlcj8KPiBzdHJpbmc/KSkpKSkKPiArwqAgOzsg RklYTUUvVE9ETyB0aGUgc2FuaXRpemUgZnVuY3Rpb24gb2Ygc3ViLWFkZHItZGVsaW0gc2hvdWxk Cj4gYWNjZXB0IGEgc3RyaW5nIG9mIGxlbmd0aCBvbmUgbm90IHN0cmluZz8KPiArwqAgKHN1Yi1h ZGRyLWRlbGltIG9wZW5zbXRwZC1zbXRwLXN1Yi1hZGRyLWRlbGltCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqAgKGRlZmF1bHQgI2YpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgKHNhbml0aXplIChsYW1iZGEgKHZhcikKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2FuaXRpemUg dmFyICJvcGVuc210cGQtc210cCIKPiAic3ViLWFkZHItZGVsaW0iCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoCAobGlzdCBmYWxzZT8gaW50ZWdlcj8KPiBzdHJpbmc/KSkpKSkpCj4gKwo+ ICsoZGVmaW5lLXJlY29yZC10eXBlKiA8b3BlbnNtdHBkLXNycz4KPiArwqAgb3BlbnNtdHBkLXNy cyBtYWtlLW9wZW5zbXRwZC1zcnMKPiArwqAgb3BlbnNtdHBkLXNycz8KPiArwqAgOzsgVE9ETyBz aG91bGQgdGhpcyBiZSBhIGZpbGU/Cj4gK8KgIChrZXkgb3BlbnNtdHBkLXNycy1rZXkKPiArwqDC oMKgwqDCoMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqAgKHNhbml0aXplIChsYW1iZGEg KHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChteS9zYW5pdGl6 ZSB2YXIgIm9wZW5zbXRwZC1zcnMiICJrZXkiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsaXN0IGZhbHNlPyBib29sZWFu PyBzdHJpbmc/KSkpKSkKPiArwqAgOzsgVE9ETyBzaG91bGQgdGhpcyBhbHNvIGJlIGEgZmlsZT8K PiArwqAgKGJhY2t1cC1rZXkgb3BlbnNtdHBkLXNycy1iYWNrdXAta2V5Cj4gK8KgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCAoc2FuaXRpemUgKGxhbWJkYSAodmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2FuaXRpemUgdmFyICJvcGVuc210cGQtc3JzIiAi YmFja3VwLQo+IGtleSIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCBmYWxzZT8gaW50ZWdl cj8pKSkpKQo+ICvCoCAodHRsLWRlbGF5IG9wZW5zbXRwZC1zcnMtdHRsLWRlbGF5Cj4gK8KgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoCAoZGVmYXVsdCAjZikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIChzYW5pdGl6ZSAobGFtYmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2FuaXRpemUgdmFyICJvcGVuc210cGQtc3JzIiAi dHRsLQo+IGRlbGF5Igo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCBmYWxzZT8gc3RyaW5nPykp KSkpKQo+ICsKPiArKGRlZmluZS1yZWNvcmQtdHlwZSogPG9wZW5zbXRwZC1xdWV1ZT4KPiArwqAg b3BlbnNtdHBkLXF1ZXVlIG1ha2Utb3BlbnNtdHBkLXF1ZXVlCj4gK8KgIG9wZW5zbXRwZC1xdWV1 ZT8KPiArwqAgKGNvbXByZXNzaW9uIG9wZW5zbXRwZC1xdWV1ZS1jb21wcmVzc2lvbgo+ICvCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFtYmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG15L3Nhbml0aXplIHZhciAib3Bl bnNtdHBkLXF1ZXVlIgo+ICJjb21wcmVzc2lvbiIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChs aXN0IGJvb2xlYW4/KSkpKSkKPiArwqAgKGVuY3J5cHRpb24gb3BlbnNtdHBkLXF1ZXVlLWVuY3J5 cHRpb24KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGRlZmF1bHQgI2YpCj4gK8KgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFtYmRhICh2YXIpCj4gK8KgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChteS9zYW5pdGl6ZSB2 YXIgIm9wZW5zbXRwZC1xdWV1ZSIKPiAiZW5jcnlwdGlvbiIKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCAobGlzdCBib29sZWFuPyBzdHJpbmc/IGZpbGUtCj4gZXhpc3RzPykpKSkpCj4gK8KgICh0dGwt ZGVsYXkgb3BlbnNtdHBkLXF1ZXVlLXR0bC1kZWxheQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgKGRlZmF1bHQgI2YpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc2FuaXRpemUgKGxh bWJkYSAodmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKG15L3Nhbml0aXplIHZhciAib3BlbnNtdHBkLXF1ZXVlIiAidHRsLQo+IGRlbGF5Igo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCBmYWxzZT8gc3RyaW5nPykpKSkpKQo+ICsKPiDCoChk ZWZpbmUtcmVjb3JkLXR5cGUqIDxvcGVuc210cGQtY29uZmlndXJhdGlvbj4KPiDCoMKgIG9wZW5z bXRwZC1jb25maWd1cmF0aW9uIG1ha2Utb3BlbnNtdHBkLWNvbmZpZ3VyYXRpb24KPiDCoMKgIG9w ZW5zbXRwZC1jb25maWd1cmF0aW9uPwo+IC3CoCAocGFja2FnZcKgwqDCoMKgIG9wZW5zbXRwZC1j b25maWd1cmF0aW9uLXBhY2thZ2UKPiAtwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZGVm YXVsdCBvcGVuc210cGQpKQo+ICvCoCAocGFja2FnZSBvcGVuc210cGQtY29uZmlndXJhdGlvbi1w YWNrYWdlCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgIChkZWZhdWx0IG9wZW5zbXRwZCkpCj4gwqDC oCAoY29uZmlnLWZpbGUgb3BlbnNtdHBkLWNvbmZpZ3VyYXRpb24tY29uZmlnLWZpbGUKPiAtwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZGVmYXVsdCAlZGVmYXVsdC1vcGVuc210cGQtY29u ZmlnLWZpbGUpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChkZWZhdWx0ICNmKSkK PiArwqAgOzsgRklYTUUvVE9ETyBzaG91bGQgSSBpbmNsdWRlIGEgYWRtZCBhdXRoc2VydmlkIGVu dHJ5Pwo+ICsKPiArwqAgOzsgVE9ETyBzYW5pdGl6ZSB0aGlzIHByb3Blcmx5IHdpdGggcGVyaGFw cyBhIDxzYW5pdGl6ZS0KPiBjb25maWd1cmF0aW9uPi4KPiArwqAgKGJvdW5jZSBvcGVuc210cGQt Y29uZmlndXJhdGlvbi1ib3VuY2UKPiArwqDCoMKgwqDCoMKgwqDCoMKgIChkZWZhdWx0ICNmKQo+ ICvCoMKgwqDCoMKgwqDCoMKgwqAgKHNhbml0aXplIChsYW1iZGEgKHZhcikKPiArwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChteS9zYW5pdGl6ZSB2YXIgIm9wZW5z bXRwZC1jb25maWd1cmF0aW9uIgo+ICJib3VuY2UiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsaXN0IGZhbHNl PyBsaXN0PykpKSkpCj4gK8KgIChjYXMgb3BlbnNtdHBkLWNvbmZpZ3VyYXRpb24tY2FzCj4gK8Kg wqDCoMKgwqDCoCAoZGVmYXVsdCAjZikKPiArwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFtYmRh ICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2FuaXRp emUgdmFyICJvcGVuc210cGQtY29uZmlndXJhdGlvbiIgImNhcyIKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3QgZmFs c2U/IGxpc3Qtb2Ytb3BlbnNtdHBkLQo+IGNhPykpKSkpCj4gK8KgIDs7IGxpc3Qgb2YgbWFueSBy ZWNvcmRzIG9mIHR5cGUgb3BlbnNtdHBkLWludGVyZmFjZQo+ICvCoCAobGlzdGVuLW9ucyBvcGVu c210cGQtY29uZmlndXJhdGlvbi1saXN0ZW4tb25zCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIChkZWZhdWx0IChsaXN0IChvcGVuc210cGQtaW50ZXJmYWNlKSkpCj4gK8KgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFtYmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChpZiAobGlzdC1vZi1vcGVuc210 cGQtaW50ZXJmYWNlPyB2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgdmFyCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGJlZ2luCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChkaXNwbGF5 ICI8b3BlbnNtdHBkLWNvbmZpZ3VyYXRpb24+Cj4gZmllbGRuYW1lICdsaXN0ZW4tb25zJyBleHBl Y3RzIGEgbGlzdCBvZiByZWNvcmRzICIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChkaXNwbGF5ICJvZiBvbmUgb3IgbW9y ZSB1bmlxdWUKPiA8b3BlbnNtdHBkLWludGVyZmFjZT4gcmVjb3Jkcy5cbiIpCj4gK8KgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICh0 aHJvdyAnYmFkISB2YXIpKSkpKSkKPiArwqAgOzsgYWNjZXB0cyB0eXBlIDxvcGVuc210cGQtc29j a2V0LWNvbmZpZ3VyYXRpb24+Cj4gK8KgIChsaXN0ZW4tb24tc29ja2V0IG9wZW5zbXRwZC1jb25m aWd1cmF0aW9uLWxpc3Rlbi1vbi1zb2NrZXQKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgKGRlZmF1bHQgKG9wZW5zbXRwZC1zb2NrZXQtY29uZmlndXJhdGlvbikpKQo+ ICvCoCAoaW5jbHVkZXMgb3BlbnNtdHBkLWNvbmZpZ3VyYXRpb24taW5jbHVkZXMgOzsgbGlzdCBv ZiBzdHJpbmdzIG9mCj4gYWJzb2x1dGUgcGF0aCBuYW1lcwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAobGFt YmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgKG15L3Nhbml0aXplIHZhciAib3BlbnNtdHBkLWNvbmZpZ3VyYXRpb24iCj4gImluY2x1ZGVz Igo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3QgZmFsc2U/IGxpc3Qtb2Ytc3RyaW5ncz8KPiBnZXhw PykpKSkpCj4gK8KgIChtYXRjaGVzIG9wZW5zbXRwZC1jb25maWd1cmF0aW9uLW1hdGNoZXMKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqAgKGRlZmF1bHQgKGxpc3QgKG9wZW5zbXRwZC1tYXRjaAo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChhY3Rp b24gKG9wZW5zbXRwZC1sb2NhbC1kZWxpdmVyeQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChuYW1lICJsb2Nh bCIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqAgKG1ldGhvZCAibWJveCIpKSkKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3B0aW9ucyAobGlzdAo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1vcHRpb24KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9w dGlvbiAiZm9yIGxvY2FsIikpKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgIChvcGVuc210cGQtbWF0Y2gKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoYWN0aW9uIChvcGVuc210cGQtcmVs YXkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoCAobmFtZSAib3V0Ym91bmQiKSkpCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wdGlvbnMgKGxpc3QKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgIChvcGVuc210cGQtb3B0aW9uCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChv cHRpb24gImZyb20gbG9jYWwiKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcGVuc210cGQtb3B0aW9u Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcHRpb24gImZvciBhbnkiKSkpKSkpKQo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoCA7OyBUT0RPIHBlcmhhcHMgSSBzaG91bGQgc2FuaXRpemUgdGhpcyBmdW5j dGlvbiBsaWtlIEkKPiBzYW5pdGl6ZWQgdGhlICdmaWx0ZXJzJy4KPiArwqDCoMKgwqDCoMKgwqDC oMKgwqAgOzsgSSBkZWZpbml0ZWx5IHNob3VsZCBzYW5pdGl6ZSB0aGlzIGZ1bmN0aW9uIGEgYml0 Cj4gbW9yZS7CoCBGb3IgZXhhbXBsZSwgeW91IGNvdWxkIGhhdmUgdHdvIGRpZmZlcmVudAo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoCA7OyBhY3Rpb25zLCBvbmUgZm9yIGxvY2FsIGRlbGl2ZXJ5IGFu ZCBvbmUgZm9yIHJlbW90ZSwKPiB3aXRoIHRoZSBzYW1lIG5hbWUuwqAgSSBzaG91bGQgbWFrZSBz dXJlIHRoYXQKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqAgOzsgSSBoYXZlIG5vIHR3byBkaWZmZXJl bnQgYWN0aW9ucyB3aXRoIHRoZSBzYW1lIG5hbWUuCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgIChz YW5pdGl6ZSAobGFtYmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIDs7IFNob3VsZCB3ZSBkbyBtb3JlIHNhbml0aXppbmcgaGVyZT/CoCBlZzoK PiAiZnJvbSBzb2NrZXQiIHNob3VsZCBOT1QgaGF2ZSBhIHRhYmxlIG9yIHZhbHVlCj4gK8KgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIHZhcgo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2FuaXRpemUgdmFyICJvcGVu c210cGQtY29uZmlndXJhdGlvbiIKPiAibWF0Y2hlcyIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCBs aXN0LW9mLXVuaXF1ZS1vcGVuc210cGQtCj4gbWF0Y2g/KSkpKSkKPiArwqAgOzsgbGlzdCBvZiBt YW55IHJlY29yZHMgb2YgdHlwZSBtZGEtd3JhcHBlcgo+ICvCoCA7OyBUT0RPL0ZJWE1FIHN1cHBv cnQgdXNpbmcgZ2V4cHMgaGVyZQo+ICvCoCA7OyBlZyAobGlzdCAibmFtZSIgZ2V4cCkKPiArwqAg KG1kYS13cmFwcGVycyBvcGVuc210cGQtY29uZmlndXJhdGlvbi1tZGEtd3JhcHBlcnMKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgKHNhbml0aXplIChsYW1iZGEgKHZhcikKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChteS9zYW5pdGl6ZSB2 YXIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIm9wZW5zbXRwZC1jb25maWd1cmF0aW9uIgo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAibWRhLXdyYXBwZXJzIgo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAobGlzdCBmYWxzZT8gc3RyaW5nPykpKSkpCj4gK8KgIChtdGEtbWF4LWRlZmVy cmVkIG9wZW5zbXRwZC1jb25maWd1cmF0aW9uLW10YS1tYXgtZGVmZXJyZWQKPiArwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGRlZmF1bHQgMTAwKQo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc2FuaXRpemUgKGxhbWJkYSAodmFyKQo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCAobXkvc2FuaXRpemUgdmFyICJvcGVuc210cGQtCj4gY29uZmlndXJhdGlvbiIgIm10YS1t YXgtZGVmZXJyZWQiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3QgbnVt YmVyPykpKSkpCj4gKwo+ICvCoCA7OyBUT0RPIHNob3VsZCBJIGFkZCBhIGZpZWxkbmFtZSBwcm9j IF9wcm9jLW5hbWVfIF9jb21tYW5kXyBhcwo+IGZvdW5kIGluIHRoZSBtYW4gNSBzbXRwZC5jb25m ID8KPiArCj4gK8KgIChxdWV1ZSBvcGVuc210cGQtY29uZmlndXJhdGlvbi1xdWV1ZQo+ICvCoMKg wqDCoMKgwqDCoMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqDCoMKgIChzYW5pdGl6ZSAo bGFtYmRhICh2YXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAg KG15L3Nhbml0aXplIHZhciAib3BlbnNtdHBkLWNvbmZpZ3VyYXRpb24iCj4gInF1ZXVlIgo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqAgKGxpc3QgZmFsc2U/IG9wZW5zbXRwZC1xdWV1ZT8pKSkpKQo+ICvCoCAoc210cCBv cGVuc210cGQtY29uZmlndXJhdGlvbi1zbXRwCj4gK8KgwqDCoMKgwqDCoMKgIChkZWZhdWx0ICNm KQo+ICvCoMKgwqDCoMKgwqDCoCAoc2FuaXRpemUgKGxhbWJkYSAodmFyKQo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobXkvc2FuaXRpemUgdmFyICJvcGVuc210cGQt Y29uZmlndXJhdGlvbiIKPiAic210cCIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCBmYWxzZT8gb3BlbnNtdHBk LXNtdHA/KSkpKSkKPiArwqAgKHNycyBvcGVuc210cGQtY29uZmlndXJhdGlvbi1zcnMKPiArwqDC oMKgwqDCoMKgIChkZWZhdWx0ICNmKQo+ICvCoMKgwqDCoMKgwqAgKHNhbml0aXplIChsYW1iZGEg KHZhcikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChteS9zYW5pdGl6 ZSB2YXIgIm9wZW5zbXRwZC1jb25maWd1cmF0aW9uIiAic3JzIgo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCBmYWxz ZT8gb3BlbnNtdHBkLXNycz8pKSkpKQo+IMKgwqAgKHNldGdpZC1jb21tYW5kcz8gb3BlbnNtdHBk LXNldGdpZC1jb21tYW5kcz8gKGRlZmF1bHQgI3QpKSkKPiDCoAo+ICs7OyB0aGlzIGhlbHAgcHJv Y2VkdXJlIGlzIHVzZWQgMyBvciA0IHRpbWVzIGJ5IHNhbml0aXplLWxpc3Qtb2YtCj4gb3B0aW9u cy1mb3ItbWF0Y2gtY29uZmlndXJhdGlvbgo+ICsoZGVmaW5lKiAodGhyb3ctZXJyb3ItZHVwbGlj YXRlLW9wdGlvbiBvcHRpb24gZXJyb3ItYXJnICM6a2V5Cj4gKHJlY29yZC1uYW1lICJtYXRjaCIp KQo+ICvCoCAodGhyb3ctZXJyb3IgZXJyb3ItYXJnCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKGxpc3QgKHN0cmluZy1hcHBlbmQgIjxvcGVuc210cGQtIiByZWNvcmQtbmFtZSAiPidz Cj4gZmllbGRuYW1lICdvcHRpb25zJyBoYXMgdHdvXG4iKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzdHJpbmctYXBwZW5kICI8b3BlbnNtdHBkLW9wdGlvbj4g cmVjb3JkcyB3aXRoCj4gZmllbGRuYW1lICdvcHRpb24nIHdpdGggdmFsdWUgJyIgb3B0aW9uICIn LiBcbiIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHN0cmlu Zy1hcHBlbmQgIllvdSBjYW4gb25seSBoYXZlIG9uZSBvcHRpb24KPiB3aXRoIHZhbHVlICciIG9w dGlvbiAiJyBpbiB0aGUgb3B0aW9ucyBsaXN0LlxuIikpKSkKPiArCj4gKzs7IHRoaXMgcHJvY2Vk dXJlIHNhbml0aXplcyB0aGUgZmllbGRuYW1lIG9wZW5zbXRwZC1tYXRjaC1vcHRpb25zCj4gKyhk ZWZpbmUqIChzYW5pdGl6ZS1saXN0LW9mLW9wdGlvbnMtZm9yLW1hdGNoLWNvbmZpZ3VyYXRpb24g JW9wdGlvbnMpCj4gK8KgIChsZXQgbG9vcCAoKCV0cmF2ZXJzaW5nLW9wdGlvbnMgJW9wdGlvbnMp Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCA7OyBzYW5pdGl6ZWQtb3B0aW9ucyBpcyBhbiBh bGlzdCB0aGF0IG1heSBlbmQgb2YKPiBsb29raW5nIGxpa2U6Cj4gK8KgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCA7OyAoKCJmb3IiIChvcGVuc210cGQtb3B0aW9uIChvcHRpb24gImZvciBhbnkiKSkp Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCA7O8KgICgiZnJvbSIgKG9wZW5zbXRwZC1vcHRp b24gKG9wdGlvbiAiZnJvbSBhbnkiKSkpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKCVz YW5pdGl6ZWQtb3B0aW9ucyAnKCkpKQo+ICvCoMKgwqAgKGlmIChudWxsPyAldHJhdmVyc2luZy1v cHRpb25zKQo+ICvCoMKgwqDCoMKgwqDCoCAocmVtb3ZlIGZhbHNlPwo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgKGxpc3QKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgKGFzc29jLXJlZiAlc2FuaXRpemVkLW9wdGlvbnMgImZvciIpCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgIChhc3NvYy1yZWYgJXNhbml0aXplZC1vcHRpb25zICJmcm9tIikK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGFzc29jLXJlZiAlc2FuaXRpemVk LW9wdGlvbnMgImF1dGgiKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoYXNz b2MtcmVmICVzYW5pdGl6ZWQtb3B0aW9ucyAiaGVsbyIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIChhc3NvYy1yZWYgJXNhbml0aXplZC1vcHRpb25zICJtYWlsLWZyb20iKQo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoYXNzb2MtcmVmICVzYW5pdGl6ZWQt b3B0aW9ucyAicmNwdC10byIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChh c3NvYy1yZWYgJXNhbml0aXplZC1vcHRpb25zICJ0YWciKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoCAoYXNzb2MtcmVmICVzYW5pdGl6ZWQtb3B0aW9ucyAidGxzIikpKQo+ICvC oMKgwqDCoMKgwqDCoCAobGV0KiAoKG9wdGlvbi1yZWNvcmQgKGNhciAldHJhdmVyc2luZy1vcHRp b25zKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3B0aW9uLXN0cmluZyAob3Bl bnNtdHBkLW9wdGlvbi1vcHRpb24gb3B0aW9uLQo+IHJlY29yZCkpKQo+ICvCoMKgwqDCoMKgwqDC oMKgwqAgKGNvbmQgKChzdHJpbmc9PyAiYXV0aCIgb3B0aW9uLXN0cmluZykKPiArwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGlmIChhc3NvYy1yZWYgJXNhbml0aXplZC1vcHRpb25z ICJhdXRoIikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAodGhy b3ctZXJyb3ItZHVwbGljYXRlLW9wdGlvbiAiYXV0aCIKPiAldHJhdmVyc2luZy1vcHRpb25zKQo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsb29wIChjZHIgJXRy YXZlcnNpbmctb3B0aW9ucykgKGFsaXN0LWNvbnMKPiAiYXV0aCIgb3B0aW9uLXJlY29yZCAlc2Fu aXRpemVkLW9wdGlvbnMpKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoKHN0 cmluZz0/ICJoZWxvIiBvcHRpb24tc3RyaW5nKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAoY29uZCBbKGFzc29jLXJlZiAlc2FuaXRpemVkLW9wdGlvbnMgImhlbG8iKQo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICh0aHJvdy1lcnJv ci1kdXBsaWNhdGUtb3B0aW9uICJoZWxvIgo+ICV0cmF2ZXJzaW5nLW9wdGlvbnMpXQo+ICvCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBbKG5vdCAob3BlbnNtdHBk LW9wdGlvbi1kYXRhIG9wdGlvbi1yZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgICh0aHJvdy1lcnJvciBvcHRpb24tcmVjb3JkCj4gK8KgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAobGlzdCAiPG9wZW5zbXRwZC1vcHRpb24+IHdpdGgKPiBmaWVsZG5hbWUgJ29w dGlvbicgd2l0aCB2YWx1ZSAnaGVsbycgXG4iCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCAibXVzdCBoYXZlIGEgJ2RhdGEnIG9mCj4gdHlwZSBzdHJpbmcgb3IgPG9wZW5zbXRwZC10YWJs ZT4uXG4iKSldCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg IFtlbHNlIChsb29wIChjZHIgJXRyYXZlcnNpbmctb3B0aW9ucykgKGFsaXN0LQo+IGNvbnMgImhl bG8iIG9wdGlvbi1yZWNvcmQgJXNhbml0aXplZC1vcHRpb25zKSldKSkKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgICgoc3RyaW5nPT8gIm1haWwtZnJvbSIgb3B0aW9uLXN0cmluZykK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGNvbmQgKChhc3NvYy1yZWYgJXNh bml0aXplZC1vcHRpb25zICJtYWlsLWZyb20iKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgICh0aHJvdy1lcnJvci1kdXBsaWNhdGUtb3B0aW9uICJtYWls LWZyb20iCj4gJXRyYXZlcnNpbmctb3B0aW9ucykpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgICgobm90IChvcGVuc210cGQtb3B0aW9uLWRhdGEgb3B0aW9u LXJlY29yZCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgKHRocm93LWVycm9yIG9wdGlvbi1yZWNvcmQKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsaXN0ICI8 b3BlbnNtdHBkLW9wdGlvbj4gd2l0aAo+IGZpZWxkbmFtZSAnb3B0aW9uJyB3aXRoIHZhbHVlICdt YWlsLWZyb20nIFxuIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIm11c3QgaGF2ZSBh ICdkYXRhJyBvZgo+IHR5cGUgc3RyaW5nIG9yIDxvcGVuc210cGQtdGFibGU+LlxuIikpKQo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZWxzZSAobG9vcCAo Y2RyICV0cmF2ZXJzaW5nLW9wdGlvbnMpIChhbGlzdC0KPiBjb25zICJtYWlsLWZyb20iIG9wdGlv bi1yZWNvcmQgJXNhbml0aXplZC1vcHRpb25zKSkpKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgICgoc3RyaW5nPT8gInJjcHQtdG8iIG9wdGlvbi1zdHJpbmcpCj4gK8KgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChjb25kIFsoYXNzb2MtcmVmICVzYW5pdGl6ZWQtb3B0 aW9ucyAicmNwdC10byIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqAgKHRocm93LWVycm9yLWR1cGxpY2F0ZS1vcHRpb24gInJjcHQtdG8iCj4gJXRyYXZl cnNpbmctb3B0aW9ucyldCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgIFsobm90IChvcGVuc210cGQtb3B0aW9uLWRhdGEgb3B0aW9uLXJlY29yZCkpCj4gK8Kg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHRocm93LWVycm9y IG9wdGlvbi1yZWNvcmQKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsaXN0ICI8b3BlbnNtdHBkLW9wdGlv bj4gd2l0aAo+IGZpZWxkbmFtZSAnb3B0aW9uJyB3aXRoIHZhbHVlICdyY3B0LXRvJyBcbiIKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJtdXN0IGhhdmUgYSAnZGF0YScgb2YKPiB0eXBl IHN0cmluZyBvciA8b3BlbnNtdHBkLXRhYmxlPi5cbiIpKV0KPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgW2Vsc2UgKGxvb3AgKGNkciAldHJhdmVyc2luZy1v cHRpb25zKSAoYWxpc3QtCj4gY29ucyAicmNwdC10byIgb3B0aW9uLXJlY29yZCAlc2FuaXRpemVk LW9wdGlvbnMpKV0pKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKChzdHJpbmc9 PyAidGFnIiBvcHRpb24tc3RyaW5nKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCAoY29uZCAoKGFzc29jLXJlZiAlc2FuaXRpemVkLW9wdGlvbnMgInRhZyIpCj4gK8KgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHRocm93LWVycm9yLWR1cGxp Y2F0ZS1vcHRpb24gInRhZyIKPiAldHJhdmVyc2luZy1vcHRpb25zKSkKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKChub3QgKHN0cmluZz8gKG9wZW5zbXRw ZC1vcHRpb24tZGF0YSBvcHRpb24tCj4gcmVjb3JkKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHRocm93LWVycm9yIG9wdGlvbi1yZWNvcmQKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgIChsaXN0ICI8b3BlbnNtdHBkLW9wdGlvbj4gd2l0aAo+IGZpZWxkbmFt ZSAnb3B0aW9uJyB3aXRoIHZhbHVlICd0YWcnIFxuIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgIm11c3QgaGF2ZSBhICdkYXRhJyBvZgo+IHR5cGUgc3RyaW5nLlxuIikpKQo+ICvCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZWxzZSAobG9vcCAoY2Ry ICV0cmF2ZXJzaW5nLW9wdGlvbnMpIChhbGlzdC0KPiBjb25zICJ0YWciIG9wdGlvbi1yZWNvcmQg JXNhbml0aXplZC1vcHRpb25zKSkpKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg ICgoc3RyaW5nPT8gInRscyIgb3B0aW9uLXN0cmluZykKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgKGNvbmQgWyhhc3NvYy1yZWYgJXNhbml0aXplZC1vcHRpb25zICJ0bHMiKQo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICh0aHJvdy1l cnJvci1kdXBsaWNhdGUtb3B0aW9uICJ0bHMiCj4gJXRyYXZlcnNpbmctb3B0aW9ucyldCj4gK8Kg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIFsob3IgKG9wZW5zbXRw ZC1vcHRpb24tZGF0YSBvcHRpb24tcmVjb3JkKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1vcHRpb24tcmVnZXggb3B0 aW9uLXJlY29yZCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKHRocm93LWVycm9yIG9wdGlvbi1yZWNvcmQKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsaXN0 ICI8b3BlbnNtdHBkLW9wdGlvbj4gd2l0aAo+IGZpZWxkbmFtZSAnb3B0aW9uJyB3aXRoIHZhbHVl ICd0bHMnLCB0aGVuIFxuIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgImZpZWxkbmFt ZSAnZGF0YScgY2Fubm90Cj4gYmUgZGVmaW5lZC5cbiIpKV0KPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgW2Vsc2UgKGxvb3AgKGNkciAldHJhdmVyc2luZy1v cHRpb25zKSAoYWxpc3QtCj4gY29ucyAidGxzIiBvcHRpb24tcmVjb3JkICVzYW5pdGl6ZWQtb3B0 aW9ucykpXSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoKHN0cmluZz0/ICJm b3IiIChzdWJzdHJpbmcgb3B0aW9uLXN0cmluZyAwIDMpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoCAoY29uZCAoKGFzc29jLXJlZiAlc2FuaXRpemVkLW9wdGlvbnMgImZvciIp Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHRocm93 LWVycm9yICVvcHRpb25zCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBgKCI8b3BlbnNtdHBkLW1hdGNoPidz Cj4gZmllbGRuYW1lICdvcHRpb25zJyBjYW4gb25seSBoYXZlIG9uZSAnZm9yJyBvcHRpb24uIFxu Igo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJCdXQgJyIgLG9wdGlvbi1zdHJpbmcgIicgYW5kCj4g JyIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAsKG9wZW5zbXRwZC1vcHRpb24tb3B0aW9uCj4gKGFz c29jLXJlZiAlc2FuaXRpemVkLW9wdGlvbnMgImZvciIpKSAiJyBhcmUgcHJlc2VudC5cbiIpKSkK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKChhbmQgKHN0 cmluZy1pbi1saXN0PyBvcHRpb24tc3RyaW5nIChsaXN0Cj4gImZvciBhbnkiICJmb3IgbG9jYWwi KSkgOyBmb3IgYW55IGNhbm5vdCBoYXZlIGEgZGF0YSBmaWVsZC4KPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9yIChvcGVuc210cGQt b3B0aW9uLWRhdGEgb3B0aW9uLQo+IHJlY29yZCkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3BlbnNtdHBkLW9wdGlv bi1yZWdleCBvcHRpb24tCj4gcmVjb3JkKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqAgKHRocm93LWVycm9yIG9wdGlvbi1yZWNvcmQKPiArwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgIChsaXN0ICJXaGVuIDxvcGVubXN0cGQtb3B0aW9uLQo+IGNvbmZpZ3VyYXRpb24+ J3MgZmllbGRuYW1lICdvcHRpb25zJyB2YWx1ZSBpcyAnZm9yIGFueScgXG4iCj4gK8KgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoCAib3IgJ2ZvciBsb2NhbCcsIHRoZW4gaXRzCj4gJ2RhdGEnIGFu ZCAncmVnZXgnIGZpZWxkIG11c3QgYmUgI2YuIFxuIikpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoKGFuZCAoc3RyaW5nLWluLWxpc3Q/IG9wdGlvbi1z dHJpbmcgKGxpc3QKPiAiZm9yIGRvbWFpbiIgImZvciByY3B0LXRvIikpIDsgZm9yIGRvbWFpbiBt dXN0IGhhdmUgYSBkYXRhIGZpZWxkLgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobm90IChvcGVuc210cGQtb3B0aW9uLWRhdGEgb3B0 aW9uLQo+IHJlY29yZCkpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgICh0aHJvdy1lcnJvciBvcHRpb24tcmVjb3JkCj4gK8KgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAo bGlzdCAiV2hlbiA8b3Blbm1zdHBkLW9wdGlvbi0KPiBjb25maWd1cmF0aW9uPidzIGZpZWxkbmFt ZSAnb3B0aW9ucycgdmFsdWUgaXMgJ2ZvciBkb21haW4nIFxuIgo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgIm9yICdmb3IgcmNwdC10bycsIHRoZW4KPiBpdHMgJ2RhdGEnIGZpZWxkIG11 c3QgYmUgYSBzdHJpbmcgb3IgYW4gXG4iCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAi PG9wZW5zbXRwZC10YWJsZT4KPiByZWNvcmQuXG4iKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChlbHNlIChsb29wIChjZHIgJXRyYXZlcnNpbmctb3B0 aW9ucykgKGFsaXN0LQo+IGNvbnMgImZvciIgb3B0aW9uLXJlY29yZCAlc2FuaXRpemVkLW9wdGlv bnMpKSkpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKChzdHJpbmc9PyAiZnJv bSIgKHN1YnN0cmluZyBvcHRpb24tc3RyaW5nIDAgNCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIChjb25kICgoYXNzb2MtcmVmICVzYW5pdGl6ZWQtb3B0aW9ucyAiZnJvbSIp Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHRocm93 LWVycm9yICVvcHRpb25zCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBgKCI8b3BlbnNtdHBkLW1hdGNoPidz Cj4gZmllbGRuYW1lICdvcHRpb25zJyBjYW4gb25seSBoYXZlIG9uZSAnZnJvbScgb3B0aW9uLiBc biIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAiQnV0ICciICxvcHRpb24tc3RyaW5nICInIGFuZAo+ ICciCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgLChvcGVuc210cGQtb3B0aW9uLW9wdGlvbgo+IChh c3NvYy1yZWYgJXNhbml0aXplZC1vcHRpb25zICJmcm9tIikpICInIGFyZSBwcmVzZW50LlxuIikp KQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoKGFuZCAo c3RyaW5nLWluLWxpc3Q/IG9wdGlvbi1zdHJpbmcgKGxpc3QKPiAiZnJvbSBhbnkiICJmcm9tIGxv Y2FsIiAiZnJvbSBzb2NrZXQiKSkgOyBmb3IgYW55IGNhbm5vdCBoYXZlIGEgZGF0YQo+IGZpZWxk Lgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCAob3IgKG9wZW5zbXRwZC1vcHRpb24tZGF0YSBvcHRpb24tCj4gcmVjb3JkKQo+ICvCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIChvcGVuc210cGQtb3B0aW9uLXJlZ2V4IG9wdGlvbi0KPiByZWNvcmQpKSkKPiArwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAodGhyb3ctZXJyb3Igb3B0 aW9uLXJlY29yZAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3QgIldoZW4gPG9wZW5tc3RwZC1vcHRp b24tCj4gY29uZmlndXJhdGlvbj4ncyBmaWVsZG5hbWUgJ29wdGlvbnMnIHZhbHVlIGlzICdmcm9t IGFueScsIFxuIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIiAnZnJvbSBsb2NhbCcs IG9yICdmcm9tCj4gc29ja2V0JywgdGhlbiBpdHMgJ2RhdGEnIGFuZCAncmVnZXgnIGZpZWxkIG11 c3QgYmUgI2YuIFxuIikpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAoKGFuZCAoc3RyaW5nLWluLWxpc3Q/IG9wdGlvbi1zdHJpbmcgKGxpc3QKPiAiZnJv bSBtYWlsLWZyb20iICJmcm9tIHNyYyIpKSA7IGZvciBkb21haW4gbXVzdCBoYXZlIGEgZGF0YSBm aWVsZC4KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqAgKG5vdCAob3BlbnNtdHBkLW9wdGlvbi1kYXRhIG9wdGlvbi0KPiByZWNvcmQpKSkK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAodGhyb3ct ZXJyb3Igb3B0aW9uLXJlY29yZAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3QgIldoZW4gPG9wZW5t c3RwZC1vcHRpb24tCj4gY29uZmlndXJhdGlvbj4ncyBmaWVsZG5hbWUgJ29wdGlvbnMnIHZhbHVl IGlzICdmcm9tIG1haWwtZnJvbScgXG4iCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAi b3IgJ2Zyb20gc3JjJywgdGhlbiBpdHMKPiAnZGF0YScgZmllbGQgbXVzdCBiZSBhIHN0cmluZyBv ciBhbiBcbiIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICI8b3BlbnNtdHBkLXRhYmxl Pgo+IHJlY29yZC5cbiIpKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqAgKGVsc2UgKGxvb3AgKGNkciAldHJhdmVyc2luZy1vcHRpb25zKSAoYWxpc3QtCj4g Y29ucyAiZnJvbSIgb3B0aW9uLXJlY29yZCAlc2FuaXRpemVkLW9wdGlvbnMpKSkpKSkpKSkpCj4g Kwo+ICs7OyBpZiB0aGUgbGlzdCBvZiBmaWx0ZXJzIGluIG9wZW5zbXRwZC1pbnRlcmZhY2UtZmls dGVycwo+ICs7OyBhbmQgaW4gb3BlbnNtdHBkLXNvY2tldC1jb25maWd1cmF0aW9uLWZpbHRlcnMg aGFzIHR3bwo+ICs7OyBmaWx0ZXJzIHdpdGggdGhlIHNhbWUgbmFtZSwgdGhpcyB3aWxsIHJldHVy biAjdAo+ICs7OyBvdGhlcndpc2UgZmFsc2UKPiArKGRlZmluZSAoZHVwbGljYXRlLWZpbHRlci1u YW1lPyAlZmlsdGVycykKPiArwqAgKGNvbnRhaW5zLWR1cGxpY2F0ZT8KPiArwqDCoCAobGV0IGxv b3AgKCglZmlsdGVycyAlZmlsdGVycykpCj4gK8KgwqDCoMKgIChpZiAobnVsbD8gJWZpbHRlcnMp Cj4gK8KgwqDCoMKgwqDCoMKgwqAgJygpCj4gK8KgwqDCoMKgwqDCoMKgwqAgKGNvbmQKPiArwqDC oMKgwqDCoMKgwqDCoMKgICgob3BlbnNtdHBkLWZpbHRlci1waGFzZT8gKGNhciAlZmlsdGVycykp Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgIChjb25zIChvcGVuc210cGQtZmlsdGVyLXBoYXNlLW5h bWUgKGNhciAlZmlsdGVycykpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChs b29wIChjZHIgJWZpbHRlcnMpKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoCAoZWxzZQo+ICvCoMKg wqDCoMKgwqDCoMKgwqDCoCAoY29ucyAob3BlbnNtdHBkLWZpbHRlci1uYW1lIChjYXIgJWZpbHRl cnMpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobG9vcCAoY2RyICVmaWx0 ZXJzKSkpKSkpKSkpCj4gKwo+ICsoZGVmaW5lIChsaXN0LWhhcy1kdXBsaWNhdGVzLW9yLW5vbi1m aWx0ZXJzPyBsaXN0KQo+ICvCoCAobm90IChsaXN0LW9mLXVuaXF1ZS1maWx0ZXItb3ItZmlsdGVy LXBoYXNlPyBsaXN0KSkpCj4gKwo+ICsoZGVmaW5lIChmaWx0ZXItcGhhc2UtaGFzLW1lc3NhZ2Ut YW5kLXZhbHVlPyByZWNvcmQpCj4gK8KgIChhbmQgKG9wZW5zbXRwZC1maWx0ZXItcGhhc2UtbWVz c2FnZSByZWNvcmQpCj4gK8KgwqDCoMKgwqDCoCAob3BlbnNtdHBkLWZpbHRlci1waGFzZS12YWx1 ZSByZWNvcmQpKSkKPiArCj4gKzs7IHJldHVybiAjdCBpZiBwaGFzZSBuZWVkcyBhIG1lc3NhZ2Uu IE9yIGlmIHRoZSBtZXNzYWdlIGRpZCBub3QKPiBzdGFydCB3aXRoIGEgNHh4IG9yIDV4eCBzdGF0 dXMgY29kZS4KPiArOzsgb3RoZXJ3aXNlICNmCj4gKyhkZWZpbmUgKGZpbHRlci1waGFzZS1kZWNp c2lvbi1sYWNrcy1wcm9wZXItbWVzc2FnZT8gcmVjb3JkKQo+ICvCoCAoZGVmaW5lIGRlY2lzaW9u IChvcGVuc210cGQtZmlsdGVyLXBoYXNlLWRlY2lzaW9uIHJlY29yZCkpCj4gK8KgIChpZiAoc3Ry aW5nLWluLWxpc3Q/IGRlY2lzaW9uIChsaXN0ICJkaXNjb25uZWN0IiAicmVqZWN0IikpCj4gK8Kg wqDCoMKgwqAgOzsgdGhpcyBtZXNzYWdlIG5lZWRzIHRvIGJlIFJGQyBjb21wbGlhbnQsIG1lYW5p bmcKPiArwqDCoMKgwqDCoCA7OyB0aGF0IGl0IG5lZWQgdG8gc3RhcnQgd2l0aCA0eHggb3IgNXh4 IHN0YXR1cyBjb2RlCj4gK8KgwqDCoMKgwqAgKGNvbmQgKChlcT8gI2YgKG9wZW5zbXRwZC1maWx0 ZXItcGhhc2UtbWVzc2FnZSByZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgI3Qp Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKChzdHJpbmc/IChvcGVuc210cGQtZmlsdGVyLXBo YXNlLW1lc3NhZ2UgcmVjb3JkKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsZXQgKChu dW1iZXIgKHN0cmluZy0+bnVtYmVyCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc3Vic3RyaW5nCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcGVuc210cGQtZmlsdGVyLXBo YXNlLW1lc3NhZ2UgcmVjb3JkKQo+IDAgMykpKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCAoaWYgKGFuZCAobnVtYmVyPyBudW1iZXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGFuZCAoPCBudW1iZXIgNjAwKSAoPiBudW1iZXIgMzk5 KSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAjZgo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgI3QpKSkpCj4gK8KgwqDCoMKgwqAgI2YpKQo+ ICsKPiArOzsgJ2RlY2lzaW9uJyAicmV3cml0ZSIgcmVxdWlyZXMgJ3ZhbHVlJyB0byBiZSBhIG51 bWJlci4KPiArKGRlZmluZSAoZmlsdGVyLXBoYXNlLWxhY2tzLXByb3Blci12YWx1ZT8gcmVjb3Jk KQo+ICvCoCAoZGVmaW5lIGRlY2lzaW9uIChvcGVuc210cGQtZmlsdGVyLXBoYXNlLWRlY2lzaW9u IHJlY29yZCkpCj4gK8KgIChpZiAoc3RyaW5nPT8gInJld3JpdGUiIGRlY2lzaW9uKQo+ICvCoMKg wqDCoMKgIChpZiAoYW5kIChudW1iZXI/IChvcGVuc210cGQtZmlsdGVyLXBoYXNlLXZhbHVlIHJl Y29yZCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGVxPyAjZiAob3BlbnNtdHBk LWZpbHRlci1waGFzZS1tZXNzYWdlIHJlY29yZCkpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqAgI2YK PiArwqDCoMKgwqDCoMKgwqDCoMKgICN0KQo+ICvCoMKgwqDCoMKgICNmKSkKPiArCj4gKzs7ICdk ZWNpc2lvbicgImp1bmsiIG9yICJieXBhc3MiIGNhbm5vdCBoYXZlIGEgbWVzc2FnZSBvciBhIHZh bHVlLgo+ICsoZGVmaW5lIChmaWx0ZXItcGhhc2UtaGFzLWluY29ycmVjdC1qdW5rLW9yLWJ5cGFz cz8gcmVjb3JkKQo+ICvCoCAoYW5kCj4gK8KgwqAgKHN0cmluZy1pbi1saXN0Pwo+ICvCoMKgwqAg KG9wZW5zbXRwZC1maWx0ZXItcGhhc2UtZGVjaXNpb24gcmVjb3JkKQo+ICvCoMKgwqAgKGxpc3Qg Imp1bmsiICJieXBhc3MiKSkKPiArwqDCoCAob3IKPiArwqDCoMKgIChvcGVuc210cGQtZmlsdGVy LXBoYXNlLXZhbHVlIHJlY29yZCkKPiArwqDCoMKgIChvcGVuc210cGQtZmlsdGVyLXBoYXNlLW1l c3NhZ2UgcmVjb3JkKSkpKQo+ICsKPiArKGRlZmluZSAoZmlsdGVyLXBoYXNlLWp1bmtzLWFmdGVy LWNvbW1pdD8gcmVjb3JkKQo+ICvCoCAoYW5kIChzdHJpbmc9PyAob3BlbnNtdHBkLWZpbHRlci1w aGFzZS1kZWNpc2lvbiByZWNvcmQpICJqdW5rIikKPiArwqDCoMKgwqDCoMKgIChzdHJpbmc9PyAo b3BlbnNtdHBkLWZpbHRlci1waGFzZS1waGFzZSByZWNvcmQpICJjb21taXQiKSkpCj4gKwo+ICs7 OyByZXR1cm5zICN0IGlmIGxpc3QgaXMgYSB1bmlxdWUgbGlzdCBvZiA8b3BlbnNtdHBkLWZpbHRl cj4gb3IKPiA8b3BlbnNtdHBkLWZpbHRlci1waGFzZT4KPiArOzsgcmV0dXJucyAjIG90aGVyd2lz ZQo+ICsoZGVmaW5lIChsaXN0LW9mLXVuaXF1ZS1maWx0ZXItb3ItZmlsdGVyLXBoYXNlPyAlZmls dGVycykKPiArwqAgKGFuZCAobGlzdD8gJWZpbHRlcnMpCj4gK8KgwqDCoMKgwqDCoCAobm90IChu dWxsPyAlZmlsdGVycykpCj4gK8KgwqDCoMKgwqDCoCA7OyB0aGlzIGxpc3QgaXMgbWFkZSB1cCBv ZiBvbmx5IDxvcGVuc210cGQtZmlsdGVyLXBoYXNlPiBvcgo+IDxvcGVuc210cGQtZmlsdGVyPgo+ ICvCoMKgwqDCoMKgwqAgKHByaW1pdGl2ZS1ldmFsCj4gK8KgwqDCoMKgwqDCoMKgIChjb25zICdh bmQgKG1hcCAobGFtYmRhIChmaWx0ZXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvciAob3BlbnNtdHBkLWZpbHRlcj8gZmlsdGVyKQo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg IChvcGVuc210cGQtZmlsdGVyLXBoYXNlPyBmaWx0ZXIpKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAlZmlsdGVycykpKQo+ICvCoMKgwqDCoMKgwqAg KG5vdCAoY29udGFpbnMtZHVwbGljYXRlPyAlZmlsdGVycykpKSkKPiArCj4gKzs7IHRoZSBzYW5p dGl6ZSBwcm9jZWR1cmVzIHVzZWQgZm9yIHNhbml0aXppbmcgPG9wZW5zbXRwZC1pbnRlcmZhY2U+ Cj4gYW5kCj4gKzs7IDxvcGVuc210cGQtc29ja2V0LWNvbmZpZ3VyYXRpb24+IGZpZWxkbmFtZSAn ZmlsdGVycycuCj4gKzs7IEl0IHByaW1hcmlseSBzYW5pdGl6ZXMgPGZpbHRlci1waGFzZXM+LsKg IFRoZSBvbmx5IHNhbml0aXphdGlvbiBpdAo+IGRvZXMKPiArOzsgZm9yIDxmaWx0ZXI+cywgaXMg bm8gbWFrZSBzdXJlIHRoZXJlIGFyZSBubyBkdXBsaWNhdGUgZmlsdGVyCj4gbmFtZXMuCj4gKyhk ZWZpbmUgKHNhbml0aXplLWZpbHRlci1waGFzZXMgJWxpc3QpCj4gK8KgIDs7IHRoZSBvcmRlciBv ZiB0aGUgZmlyc3QgdHdvIHRlc3RzIGluIHRoaXMgY29uZCBpcyBpbXBvcnRhbnQuCj4gK8KgIDs7 IChmYWxzZT8pIGhhcyB0byBiZSAxc3QgYW5kIChsaXN0LWhhcy1kdXBsaWNhdGVzLW9yLW5vbi0K PiBmaWx0ZXJzPykgaGFzIHRvIGJlIHNlY29uZC4KPiArwqAgOzsgWW91IG1heSBvcHRpb25hbGx5 IHJlLW9yZGVyIHRoZSBvdGhlciBhbHRlcm5hdGVzIGluIHRoZSBjb25kLgo+ICvCoCAoY29uZCAo KGZhbHNlPyAlbGlzdCkKPiArwqDCoMKgwqDCoMKgwqDCoCAjZikKPiArwqDCoMKgwqDCoMKgwqAg KChsaXN0LWhhcy1kdXBsaWNhdGVzLW9yLW5vbi1maWx0ZXJzPyAlbGlzdCkKPiArwqDCoMKgwqDC oMKgwqDCoCAoYmVnaW4KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGRpc3BsYXkgKHN0cmluZy1h cHBlbmQgIjxvcGVuc210cGQtaW50ZXJmYWNlPiBmaWVsZG5hbWU6Cj4gJ2ZpbHRlcnMnIGlzIGEg bGlzdCwgaW4gd2hpY2ggZWFjaCB1bmlxdWUgZWxlbWVudCBcbiIKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgImlz IG9mIHR5cGUgPG9wZW5zbXRwZC1maWx0ZXI+IG9yCj4gPG9wZW5zbXRwZC1maWx0ZXItcGhhc2U+ LlxuIikpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgICh0aHJvdyAnYmFkISAlbGlzdCkpKQo+ICvC oMKgwqDCoMKgwqDCoCAoKGR1cGxpY2F0ZS1maWx0ZXItbmFtZT8gJWxpc3QpCj4gK8KgwqDCoMKg wqDCoMKgwqAgKHRocm93LWVycm9yICVsaXN0IChsaXN0ICJoYXMgYSBkdXBsaWNhdGUgZmlsdGVy IG5hbWUuXG4iKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAg IzpyZWNvcmQtbmFtZSAiaW50ZXJmYWNlIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgIzpmaWVsZG5hbWUgImZpbHRlcnMiKSkKPiArwqDCoMKgwqDCoMKgwqAg KGVsc2UKPiArwqDCoMKgwqDCoMKgwqDCoCAobGV0IGxvb3AgKFsldHJhdmVyc2luZy1saXN0ICVs aXN0XQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBbJW9yaWdpbmFs LWxpc3QgJWxpc3RdKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoCAoaWYgKG51bGw/ICV0cmF2ZXJz aW5nLWxpc3QpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgJW9yaWdpbmFsLWxpc3QK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoY29uZCBbKG9wZW5zbXRwZC1maWx0ZXI/ IChjYXIgJXRyYXZlcnNpbmctbGlzdCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoCAobG9vcCAoY2RyICV0cmF2ZXJzaW5nLWxpc3QpICVvcmlnaW5hbC1saXN0 KV0KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBbKGZpbHRlci1w aGFzZS1oYXMtbWVzc2FnZS1hbmQtdmFsdWU/IChjYXIKPiAldHJhdmVyc2luZy1saXN0KSkKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChiZWdpbgo+ICvCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChkaXNwbGF5IChzdHJp bmctYXBwZW5kICI8b3BlbnNtdHBkLWZpbHRlci0KPiBwaGFzZT4gY2Fubm90IGhhdmUgZGVmaW5l ZCBmaWVsZG5hbWVzICd2YWx1ZScgXG4iCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqAgImFuZCAnbWVzc2FnZScuXG4iKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAodGhyb3cgJ2JhZCEgKGNhciAldHJhdmVyc2luZy1saXN0 KSkpXQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIFsoZmlsdGVy LXBoYXNlLWRlY2lzaW9uLWxhY2tzLXByb3Blci1tZXNzYWdlPwo+IChjYXIgJXRyYXZlcnNpbmct bGlzdCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoYmVn aW4KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZGlz cGxheSAoc3RyaW5nLWFwcGVuZCAiPG9wZW5zbXRwZC1maWx0ZXItCj4gcGhhc2U+IGZpZWxkbmFt ZTogJ2RlY2lzaW9uJyBvcHRpb25zIFxuIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgICJcImRpc2Nvbm5lY3RcIiBhbmQKPiBcInJlamVjdFwiIHJlcXVpcmUgZmllbGRu YW1lICdtZXNzYWdlJyB0byBoYXZlIGFuIFJGQyBcbiIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoCAiY29tcGxpYW50IHN0cmluZywKPiB3aGljaCBtZWFucyB0aGF0IHRo ZSBzdHJpbmcgbXVzdCBiZWdpbiB3aXRoIGEgNHh4IG9yIDV4eCBzdGF0dXMKPiBjb2RlLlxuIikp Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHRocm93 ICdiYWQhIChjYXIgJXRyYXZlcnNpbmctbGlzdCkpKV0KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoCBbKGZpbHRlci1waGFzZS1sYWNrcy1wcm9wZXItdmFsdWU/IChj YXIKPiAldHJhdmVyc2luZy1saXN0KSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIChiZWdpbgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIChkaXNwbGF5IChzdHJpbmctYXBwZW5kICI8b3BlbnNtdHBkLWZpbHRlci0K PiBwaGFzZT4gZmllbGRuYW1lOiAnZGVjaXNpb24nIG9wdGlvbiBcbiIKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAiXCJyZXdyaXRlXCIKPiByZXF1aXJlcyBmaWVsZG5h bWUgJ3ZhbHVlJyB0byBoYXZlIGEgbnVtYmVyLlxuIikpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHRocm93ICdiYWQhIChjYXIgJXRyYXZlcnNpbmct bGlzdCkpKV0KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBbKGZp bHRlci1waGFzZS1oYXMtaW5jb3JyZWN0LWp1bmstb3ItYnlwYXNzPwo+IChjYXIgJXRyYXZlcnNp bmctbGlzdCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAo YmVnaW4KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAo ZGlzcGxheSAoc3RyaW5nLWFwcGVuZCAiPG9wZW5zbXRwZC1maWx0ZXItCj4gcGhhc2U+IGZpZWxk bmFtZSAnZGVjaXNpb24nIG9wdGlvbiBcbiIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAiXCJqdW5rXCIgb3IKPiAnYnlwYXNzJyBjYW5ub3QgaGF2ZSBhIGRlZmluZWQg ZmllbGRuYW1lcyAnbWVzc2FnZScgb3IgJ3ZhbHVlJy5cbiIpKQo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICh0aHJvdyAnYmFkISAoY2FyICV0cmF2ZXJz aW5nLWxpc3QpKSldCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAg WyhmaWx0ZXItcGhhc2UtanVua3MtYWZ0ZXItY29tbWl0PyAoY2FyCj4gJXRyYXZlcnNpbmctbGlz dCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoYmVnaW4K PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZGlzcGxh eSAoc3RyaW5nLWFwcGVuZCAiPG9wZW5zbXRwZC1maWx0ZXItCj4gcGhhc2U+IGZpZWxkbmFtZSAn ZGVjaXNpb24nIG9wdGlvbiBcbiIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCAiXCJqdW5rXCIgY2Fubm90Cj4ganVuayBhbiBlbWFpbCBkdXJpbmcgJ3BoYXNlJyBcImNv bW1pdFwiLlxuIikpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKHRocm93ICdiYWQhIChjYXIgJXRyYXZlcnNpbmctbGlzdCkpKV0KPiArwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBbZWxzZSAobG9vcCAoY2RyICV0cmF2ZXJz aW5nLWxpc3QpICVvcmlnaW5hbC0KPiBsaXN0KV0pKSkpKSkKPiArCj4gKyhkZWZpbmUqIChzYW5p dGl6ZS1vcHRpb25zLWZvci1maWx0ZXItcGhhc2UtY29uZmlndXJhdGlvbiAlb3B0aW9ucykKPiAr wqAgKGlmIChmYWxzZT8gJW9wdGlvbnMpCj4gK8KgwqDCoMKgwqAgKHRocm93LWVycm9yICNmCj4g K8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdCAibXVzdCBoYXZlIGF0 IGxlYXN0IG9uZSBvcGVuc210cGQtb3B0aW9uCj4gcmVjb3JkLiIpCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAjOnJlY29yZC1uYW1lICJmaWx0ZXItcGhhc2UiCj4gK8Kg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAjOmZpZWxkbmFtZSAib3B0aW9ucyIp Cj4gK8KgwqDCoMKgwqAgKGxldCBsb29wICgoJXRyYXZlcnNpbmctb3B0aW9ucyAlb3B0aW9ucykK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgOzsgc2FuaXRpemVkLW9wdGlvbnMg aXMgYW4gYWxpc3QgdGhhdCBtYXkgZW5kIG9mCj4gbG9va2luZyBsaWtlOgo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCA7OyAoKCJmb3IiIChvcGVuc210cGQtb3B0aW9uIChvcHRp b24gImZvciBhbnkiKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIDs7wqAg KCJmcm9tIiAob3BlbnNtdHBkLW9wdGlvbiAob3B0aW9uICJmcm9tCj4gYW55IikpKSkKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKCVzYW5pdGl6ZWQtb3B0aW9ucyAnKCkpKQo+ ICvCoMKgwqDCoMKgwqDCoCAoaWYgKG51bGw/ICV0cmF2ZXJzaW5nLW9wdGlvbnMpCj4gK8KgwqDC oMKgwqDCoMKgwqDCoMKgwqAgKHJlbW92ZSBmYWxzZT8KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgKGxpc3QKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAoYXNzb2MtcmVmICVzYW5pdGl6ZWQtb3B0aW9ucyAiZmNyZG5zIikKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoYXNzb2MtcmVmICVzYW5pdGl6 ZWQtb3B0aW9ucyAicmRucyIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKGFzc29jLXJlZiAlc2FuaXRpemVkLW9wdGlvbnMgInNyYyIpCj4gK8KgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGFzc29jLXJlZiAlc2FuaXRpemVkLW9wdGlv bnMgImhlbG8iKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChh c3NvYy1yZWYgJXNhbml0aXplZC1vcHRpb25zICJhdXRoIikKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoYXNzb2MtcmVmICVzYW5pdGl6ZWQtb3B0aW9ucyAibWFp bC1mcm9tIikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoYXNz b2MtcmVmICVzYW5pdGl6ZWQtb3B0aW9ucyAicmNwdC10byIpKSkKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAobGV0KiAoKG9wdGlvbi1yZWNvcmQgKGNhciAldHJhdmVyc2luZy1vcHRpb25zKSkK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcHRpb24tc3RyaW5nIChv cGVuc210cGQtb3B0aW9uLW9wdGlvbiBvcHRpb24tCj4gcmVjb3JkKSkpCj4gK8KgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgIChjb25kICgoYXNzb2MtcmVmICVzYW5pdGl6ZWQtb3B0aW9ucyBvcHRp b24tc3RyaW5nKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIDs7 IGlmIHdlIHNlZSB0d28gInJkbnMiIChmb3IgZXhhbXBsZSksIHRocm93IGEKPiAiZHVwbGljYXRl Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgOzsgb3B0aW9uIiBl cnJvci4KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAodGhyb3ct ZXJyb3ItZHVwbGljYXRlLW9wdGlvbiBvcHRpb24tc3RyaW5nCj4gb3B0aW9uLXJlY29yZAo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICM6cmVjb3JkLW5hbWUK PiAiZmlsdGVyLXBoYXNlIikpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIDs7IHRoZSBuZXh0IDQgb3B0aW9ucyBtdXN0IGhhdmUgZmllbGRuYW1lICdkYXRhJwo+IGRl ZmluZWQuCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICgob3IgKHN0 cmluZz0/IG9wdGlvbi1zdHJpbmcgInNyYyIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc3RyaW5nPT8gb3B0aW9uLXN0cmluZyAiaGVsbyIpCj4g K8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc3RyaW5n PT8gb3B0aW9uLXN0cmluZyAibWFpbC1mcm9tIikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzdHJpbmc9PyBvcHRpb24tc3RyaW5nICJyY3B0LXRv IikpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGlmIChub3Qg KG9wZW5zbXRwZC10YWJsZT8KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcGVuc210cGQtb3B0aW9uLWRhdGEgb3B0aW9uLQo+ IHJlY29yZCkpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKHRocm93LWVycm9yIG9wdGlvbi1yZWNvcmQgKGxpc3QgIm11c3QgaGF2ZQo+IGZpZWxk bmFtZSAnZGF0YScgZGVmaW5lZC5cbiIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICM6cmVjb3JkLW5h bWUgIm9wdGlvbiIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIzpmaWVsZG5hbWUgb3B0aW9uLXN0cmlu ZykKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChs b29wIChjZHIgJXRyYXZlcnNpbmctb3B0aW9ucykKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChhbGlzdC1jb25zIG9wdGlvbi1z dHJpbmcgb3B0aW9uLQo+IHJlY29yZCAlc2FuaXRpemVkLW9wdGlvbnMpKSkpCj4gK8KgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIDs7ZmNyZG5zIGNhbm5vdCBoYXZlIGZpZWxk bmFtZSBkYXRhIGRlZmluZWQKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgKChzdHJpbmc9PyAiZmNyZG5zIiBvcHRpb24tc3RyaW5nKQo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChpZiAob3BlbnNtdHBkLW9wdGlvbi1kYXRhIG9wdGlv bi1yZWNvcmQpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCAodGhyb3ctZXJyb3Igb3B0aW9uLXJlY29yZCAobGlzdCAiY2Fubm90Cj4gaGF2ZSBmaWVs ZG5hbWUgZGF0YSBkZWZpbmVkLlxuIikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIzpyZWNvcmQtbmFt ZSAib3B0aW9uIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAjOmZpZWxkbmFtZSAicmRucyIpCj4gK8Kg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobG9vcCAoY2Ry ICV0cmF2ZXJzaW5nLW9wdGlvbnMpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoYWxpc3QtY29ucyAiZmNyZG5zIiBvcHRpb24t cmVjb3JkCj4gJXNhbml0aXplZC1vcHRpb25zKSkpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoCA7OyByZG5zIGFuZCBhdXRoIGNhbm5vdCBiZSBtYWRlIGludmFsaWRs eTsgc2tpcAo+IHRlc3RpbmcgdGhlbS4KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqAgKChvciAoc3RyaW5nPT8gInJkbnMiIG9wdGlvbi1zdHJpbmcpCj4gK8KgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc3RyaW5nPT8gImF1dGgi IG9wdGlvbi1zdHJpbmcpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIChsb29wIChjZHIgJXRyYXZlcnNpbmctb3B0aW9ucykKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoYWxpc3QtY29ucyAiYXV0aCIgb3B0 aW9uLXJlY29yZAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICVzYW5pdGl6ZWQtb3B0aW9ucykpKQo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZWxzZSAodGhyb3ctZXJy b3Igb3B0aW9uLXJlY29yZAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsaXN0ICJoYXMgYW4gaW52 YWxpZCBvcHRpb24KPiBuYW1lLiIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIzpyZWNvcmQtbmFt ZSAiZmlsdGVyLXBoYXNlIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICM6ZmllbGRuYW1lIG9wdGlv bi0KPiBzdHJpbmcpKSkpKSkpKQo+ICsKPiArKGRlZmluZSogKHRocm93LWVycm9yIHZhciAlc3Ry aW5ncwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIzprZXkK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChyZWNvcmQtbmFt ZSAjZikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChmaWVs ZG5hbWUgI2YpKQo+ICvCoCAoaWYgKGFuZCByZWNvcmQtbmFtZSBmaWVsZG5hbWUpCj4gK8KgwqDC oMKgwqAgKGJlZ2luCj4gK8KgwqDCoMKgwqDCoMKgIChkaXNwbGF5IChzdHJpbmctYXBwZW5kICI8 b3BlbnNtdHBkLSIgcmVjb3JkLW5hbWUgIj4KPiBmaWVsZG5hbWUgIiBmaWVsZG5hbWUgIiAiCj4g K8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgIChhcHBseSBzdHJpbmctYXBwZW5kICVzdHJpbmdzKSkpCj4gK8KgwqDCoMKgwqDCoMKg ICh0aHJvdyAnYmFkISB2YXIpKQo+ICvCoMKgwqDCoMKgIChiZWdpbgo+ICvCoMKgwqDCoMKgwqDC oCAoZGlzcGxheSAoYXBwbHkgc3RyaW5nLWFwcGVuZCAlc3RyaW5ncykpCj4gK8KgwqDCoMKgwqDC oMKgICh0aHJvdyAnYmFkISB2YXIpKSkpCj4gKwo+ICs7OyB0aGlzIGlzIHVzZWQgZm9yIHNhbml0 aXppbmcgPG9wZW5zbXRwZC1maWx0ZXItcGhhc2U+IGZpZWxkbmFtZQo+ICdvcHRpb25zJwo+ICso ZGVmaW5lIChjb250YWlucy1kdXBsaWNhdGU/IGxpc3QpCj4gK8KgIChpZiAobnVsbD8gbGlzdCkK PiArwqDCoMKgwqDCoCAjZgo+ICvCoMKgwqDCoMKgIChvcgo+ICvCoMKgwqDCoMKgIDs7IGNoZWNr IGlmIChjYXIgbGlzdCkgaXMgaW4gKGNkciBsaXN0KQo+ICvCoMKgwqDCoMKgwqAgKHByaW1pdGl2 ZS1ldmFsIChjb25zICdvcgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobWFwIChsYW1iZGEgKHZhcikgKGVxdWFsPyB2YXIg KGNhcgo+IGxpc3QpKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChjZHIgbGlzdCkpKSkKPiArwqDCoMKg wqDCoMKgIDs7IGNoZWNrIGlmIChjZHIgbGlzdCkgY29udGFpbnMgZHVwbGljYXRlCj4gK8KgwqDC oMKgwqDCoCAoY29udGFpbnMtZHVwbGljYXRlPyAoY2RyIGxpc3QpKSkpKQo+ICsKPiArOzsgZ2l2 ZW4gYSBsaXN0IGFuZCBwcm9jZWR1cmUsIHRoaXMgdGVzdHMgdGhhdCBlYWNoIGVsZW1lbnQgb2Yg bGlzdAo+IGlzIG9mIHR5cGUKPiArOzsgaWU6IChsaXN0LW9mLXR5cGU/IGxpc3Qgc3RyaW5nPykg dGVzdHMgZWFjaCBsaXN0IGlzIG9mIHR5cGUKPiBzdHJpbmcuCj4gKyhkZWZpbmUgKGxpc3Qtb2Yt dHlwZT8gbGlzdCBwcm9jPykKPiArwqAgKGlmIChhbmQgKGxpc3Q/IGxpc3QpCj4gK8KgwqDCoMKg wqDCoMKgwqDCoMKgIChub3QgKG51bGw/IGxpc3QpKSkKPiArwqDCoMKgwqDCoCAobGV0IGxvb3Ag KChsaXN0IGxpc3QpKQo+ICvCoMKgwqDCoMKgwqDCoCAoaWYgKG51bGw/IGxpc3QpCj4gK8KgwqDC oMKgwqDCoMKgwqDCoMKgwqAgI3QKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoaWYgKHByb2M/ IChjYXIgbGlzdCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobG9vcCAoY2Ry IGxpc3QpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgI2YpKSkKPiArwqDCoMKg wqDCoCAjZikpCj4gKwo+ICsoZGVmaW5lIChsaXN0LW9mLXN0cmluZ3M/IGxpc3QpCj4gK8KgIChs aXN0LW9mLXR5cGU/IGxpc3Qgc3RyaW5nPykpCj4gKwo+ICsoZGVmaW5lIChsaXN0LW9mLXVuaXF1 ZS1vcGVuc210cGQtb3B0aW9uPyBsaXN0KQo+ICvCoCAoYW5kIChsaXN0LW9mLXR5cGU/Cj4gK8Kg wqDCoMKgwqDCoMKgIGxpc3Qgb3BlbnNtdHBkLW9wdGlvbj8pCj4gK8KgwqDCoMKgwqDCoCAobm90 IChjb250YWlucy1kdXBsaWNhdGU/IGxpc3QpKSkpCj4gKwo+ICsoZGVmaW5lIChsaXN0LW9mLW9w ZW5zbXRwZC1jYT8gbGlzdCkKPiArwqAgKGxpc3Qtb2YtdHlwZT8gbGlzdCBvcGVuc210cGQtY2E/ KSkKPiArCj4gKyhkZWZpbmUgKGxpc3Qtb2Ytb3BlbnNtdHBkLXBraT8gbGlzdCkKPiArwqAgKGxp c3Qtb2YtdHlwZT8gbGlzdCBvcGVuc210cGQtcGtpPykpCj4gKwo+ICsoZGVmaW5lIChsaXN0LW9m LW9wZW5zbXRwZC1pbnRlcmZhY2U/IGxpc3QpCj4gK8KgIChhbmQgKGxpc3Qtb2YtdHlwZT8gbGlz dCBvcGVuc210cGQtaW50ZXJmYWNlPykKPiArwqDCoMKgwqDCoMKgIChub3QgKGNvbnRhaW5zLWR1 cGxpY2F0ZT8gbGlzdCkpKSkKPiArCj4gKyhkZWZpbmUgKGxpc3Qtb2YtdW5pcXVlLW9wZW5zbXRw ZC1tYXRjaD8gbGlzdCkKPiArwqAgKGFuZCAobGlzdC1vZi10eXBlPyBsaXN0IG9wZW5zbXRwZC1t YXRjaD8pCj4gK8KgwqDCoMKgwqDCoCAobm90IChjb250YWlucy1kdXBsaWNhdGU/IGxpc3QpKSkp Cj4gKwo+ICsoZGVmaW5lKiAobGlzdC1vZi1zdHJpbmdzLT5zdHJpbmcgbGlzdAo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgIzprZXkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzdHJpbmctZGVsaW1pdGVyICIsICIpCj4gK8KgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCAocG9zdHBlbmQgIiIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoYXBwZW5kICIiKQo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGRy b3AtcmlnaHQtbnVtYmVyIDIpKQo+ICvCoCAoc3RyaW5nLWRyb3AtcmlnaHQKPiArwqDCoCAoc3Ry aW5nLWFwcGVuZCAobGV0IGxvb3AgKChsaXN0IGxpc3QpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoCAoaWYgKG51bGw/IGxpc3QpCj4gK8KgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIiIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc3RyaW5nLWFwcGVuZCBhcHBlbmQgKGNhciBsaXN0 KSBwb3N0cGVuZAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIHN0cmluZy1kZWxpbWl0ZXIKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoCAobG9vcCAoY2RyIGxpc3QpKSkpKQo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgIGFwcGVuZCkKPiArwqDCoCBkcm9wLXJpZ2h0LW51bWJlcikpCj4g Kwo+ICs7OyBhdCB0aGUgbW9tZW50IEkgY2Fubm90IGRlZmluZSB0aGlzIGJ5IHVzaW5nIGxpc3Qt b2YtdHlwZT8KPiArOzsgdGhlIGZpcnN0IChub3QgKG51bGw/IGFzc29jLWxpc3QpKSBwcmV2ZW50 cyB0aGF0Lgo+ICsoZGVmaW5lIChhc3NvYy1saXN0PyBhc3NvYy1saXN0KQo+ICvCoCAobGlzdC1v Zi10eXBlPyBhc3NvYy1saXN0IChsYW1iZGEgKHBhaXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGlmIChhbmQgKHBhaXI/IHBh aXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHN0cmluZz8gKGNhciBwYWlyKSkKPiArwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoCAoc3RyaW5nPyAoY2RyIHBhaXIpKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICN0Cj4gK8Kg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAjZikpKSkKPiArCj4gKyhkZWZpbmUqICh2YXJpYWJsZS0+c3RyaW5nIHZhciAjOmtl eSAoYXBwZW5kICIiKSAocG9zdHBlbmQgIiAiKSkKPiArwqAgKGxldCAoKHZhciAoaWYgKG51bWJl cj8gdmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobnVtYmVyLT5zdHJp bmcgdmFyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCB2YXIpKSkKPiArwqDC oMKgIChpZiB2YXIKPiArwqDCoMKgwqDCoMKgwqAgKHN0cmluZy1hcHBlbmQgYXBwZW5kIHZhciBw b3N0cGVuZCkKPiArwqDCoMKgwqDCoMKgwqAgIiIpKSkKPiArCj4gKzs7IHRoaXMgcHJvY2VkdXJl IHRha2VzIGluIG9uZSBhcmd1bWVudC4KPiArOzsgaWYgdGhhdCBhcmd1bWVudCBpcyBhbiA8b3Bl bnNtdHBkLXRhYmxlPiB3aG9zZSBmaWVsZG5hbWUgJ3ZhbHVlcycKPiBpcyBhbiBhc3NvYy1saXN0 LCB0aGVuIGl0IHJldHVybnMKPiArOzsgI3QsICNmIGlmIG90aGVyd2lzZS4KPiArOzsgVE9ETyBz aG91bGQgSSByZW1vdmUgdGhlc2UgdHdvIGZ1bmN0aW9ucz/CoCBBbmQgaW5zdGVhZCB1c2UgdGhl Cj4gKG9wZW5zbXRwZC10YWJsZS10eXBlKSBwcm9jZWR1cmU/Cj4gKyhkZWZpbmUgKHRhYmxlLXdo b3NlLWRhdGEtYXJlLWFzc29jLWxpc3Q/IHRhYmxlKQo+ICvCoCAoaWYgKG5vdCAob3BlbnNtdHBk LXRhYmxlPyB0YWJsZSkpCj4gK8KgwqDCoMKgwqAgI2YKPiArwqDCoMKgwqDCoCAoYXNzb2MtbGlz dD8gKG9wZW5zbXRwZC10YWJsZS1kYXRhIHRhYmxlKSkpKQo+ICsKPiArOzsgdGhpcyBwcm9jZWR1 cmUgdGFrZXMgaW4gb25lIGFyZ3VtZW50Cj4gKzs7IGlmIHRoYXQgYXJndW1lbnQgaXMgYW4gPG9w ZW5zbXRwZC10YWJsZT4gd2hvc2UgZmllbGRuYW1lICd2YWx1ZXMnCj4gaXMgYSBsaXN0IG9mIHN0 cmluZ3MsIHRoZW4gaXQgcmV0dXJucwo+ICs7OyAjdCwgI2YgaWYgb3RoZXJ3aXNlLgo+ICsoZGVm aW5lICh0YWJsZS13aG9zZS1kYXRhLWFyZS1hLWxpc3Qtb2Ytc3RyaW5ncz8gdGFibGUpCj4gK8Kg IChpZiAobm90IChvcGVuc210cGQtdGFibGU/IHRhYmxlKSkKPiArwqDCoMKgwqDCoCAjZgo+ICvC oMKgwqDCoMKgIChsaXN0LW9mLXN0cmluZ3M/IChvcGVuc210cGQtdGFibGUtZGF0YSB0YWJsZSkp KSkKPiArCj4gKzs7IHRoZXNlIG5leHQgZmV3IGZ1bmN0aW9ucyBoZWxwIG1lIHRvIHR1cm4gPHRh YmxlPnMKPiArOzsgaW50byBzdHJpbmdzIHN1aXRhYmxlIHRvIGZpdCBpbnRvICJvcGVuc210cGQu Y29uZiIuCj4gKyhkZWZpbmUgKGFzc29jLWxpc3QtPnN0cmluZyBhc3NvYy1saXN0KQo+ICvCoCAo c3RyaW5nLWRyb3AtcmlnaHQKPiArwqDCoCAobGV0IGxvb3AgKChhc3NvYy1saXN0IGFzc29jLWxp c3QpKQo+ICvCoMKgwqDCoCAoaWYgKG51bGw/IGFzc29jLWxpc3QpCj4gK8KgwqDCoMKgwqDCoMKg wqAgIiIKPiArwqDCoMKgwqDCoMKgwqDCoCA7OyBwYWlyIGlzIChjb25zICJoZWxsbyIgIndvcmxk IikgLT4gKCJoZWxsbyIgLiAid29ybGQiKQo+ICvCoMKgwqDCoMKgwqDCoMKgIChsZXQgKChwYWly IChjYXIgYXNzb2MtbGlzdCkpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc3RyaW5nLWFwcGVu ZAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJcIiIgKGNhciBwYWlyKcKgICJcIiIKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoCAiID0gIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJcIiIg KGNkciBwYWlyKSAiXCIiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIiwgIgo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgIChsb29wIChjZHIgYXNzb2MtbGlzdCkpKSkpKQo+ICvCoMKgIDIpKQo+ ICsKPiArOzvCoCBUaGUgZm9sbG93aW5nIGZ1bmN0aW9ucyBjb252ZXJ0IHZhcmlvdXMgcmVjb3Jk cyBpbnRvIHN0cmluZ3MuCj4gKzs7Cj4gKzs7IGNhbiBiZSBvZiB0eXBlOiAocXVvdGUgbGlzdC1v Zi1zdHJpbmdzKSBvciAocXVvdGUgYXNzb2MtbGlzdCkKPiArKGRlZmluZSAob3BlbnNtdHBkLXRh YmxlLT5zdHJpbmcgdGFibGUpCj4gK8KgIChzdHJpbmctYXBwZW5kICJ0YWJsZSAiIChvcGVuc210 cGQtdGFibGUtbmFtZSB0YWJsZSkgIiAiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIChsZXQgKCh0eXBlIChvcGVuc210cGQtdGFibGUtdHlwZSB0YWJsZSkpKQo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGNvbmQgKChlcT8gdHlwZSAocXVvdGUgbGlz dC1vZi1zdHJpbmdzKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgKHN0cmluZy1hcHBlbmQgInsgIiAobGlzdC1vZi1zdHJpbmdzLQo+ID5zdHJp bmcgKG9wZW5zbXRwZC10YWJsZS1kYXRhIHRhYmxlKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg Cj4gwqDCoCAjOmFwcGVuZCAiXCIiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAKPiDCoMKgICM6 ZHJvcC1yaWdodC1udW1iZXIgMwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgCj4gwqDCoCAjOnBv c3RwZW5kICJcIiIpICIgfSIpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgKChlcT8gdHlwZSAocXVvdGUgYXNzb2MtbGlzdCkpCj4gK8KgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzdHJpbmctYXBwZW5k ICJ7ICIgKGFzc29jLWxpc3QtPnN0cmluZwo+IChvcGVuc210cGQtdGFibGUtZGF0YSB0YWJsZSkp ICIgfSIpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgKChlcT8gdHlwZSAocXVvdGUgZGIpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc3RyaW5nLWFwcGVuZCAiZGI6IiAob3BlbnNtdHBkLXRh YmxlLWRhdGEKPiB0YWJsZSkpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgKChlcT8gdHlwZSAocXVvdGUgZmlsZSkpCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzdHJpbmctYXBwZW5kICJmaWxl OiIgKG9wZW5zbXRwZC10YWJsZS0KPiBkYXRhIHRhYmxlKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZWxzZSAodGhyb3cgJ3lvdU1lc3NlZFVw IHRhYmxlKSkpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAiIFxuIikpCj4g Kwo+ICsoZGVmaW5lIChvcGVuc210cGQtaW50ZXJmYWNlLT5zdHJpbmcgcmVjb3JkKQo+ICvCoCAo c3RyaW5nLWFwcGVuZCAibGlzdGVuIG9uICIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKG9wZW5zbXRwZC1pbnRlcmZhY2UtaW50ZXJmYWNlIHJlY29yZCkgIiAiCj4gK8KgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsZXQqICgoaG9zdG5hbWUgKG9wZW5zbXRwZC1p bnRlcmZhY2UtaG9zdG5hbWUKPiByZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgIChob3N0bmFtZXMgKGlmIChvcGVuc210cGQtaW50ZXJmYWNl LQo+IGhvc3RuYW1lcyByZWNvcmQpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC10 YWJsZS1uYW1lCj4gKG9wZW5zbXRwZC1pbnRlcmZhY2UtaG9zdG5hbWVzIHJlY29yZCkpCj4gK8Kg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgI2YpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgIChmaWx0ZXJzIChvcGVuc210cGQtaW50ZXJmYWNlLWZpbHRlcnMKPiBy ZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg IChmaWx0ZXItbmFtZSAoaWYgZmlsdGVycwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoaWYg KDwgMSAobGVuZ3RoIGZpbHRlcnMpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg IChnZW5lcmF0ZS1maWx0ZXItY2hhaW4tCj4gbmFtZSBmaWx0ZXJzKQo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgIChpZiAob3BlbnNtdHBkLWZpbHRlcj8KPiAoY2FyIGZpbHRlcnMp KQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1m aWx0ZXItCj4gbmFtZSAoY2FyIGZpbHRlcnMpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1maWx0ZXItCj4gcGhhc2UtbmFtZSAoY2FyIGZpbHRl cnMpKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICNmKSkKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobWFzay1zcmMgKG9wZW5zbXRwZC1pbnRl cmZhY2UtbWFzay1zcmMKPiByZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgICh0YWcgKG9wZW5zbXRwZC1pbnRlcmZhY2UtdGFnIHJlY29yZCkp Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHNlY3Vy ZS1jb25uZWN0aW9uIChvcGVuc210cGQtaW50ZXJmYWNlLQo+IHNlY3VyZS1jb25uZWN0aW9uIHJl Y29yZCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAg KHBvcnQgKG9wZW5zbXRwZC1pbnRlcmZhY2UtcG9ydCByZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChwa2kgKG9wZW5zbXRwZC1pbnRlcmZh Y2UtcGtpIHJlY29yZCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqAgKGF1dGggKG9wZW5zbXRwZC1pbnRlcmZhY2UtYXV0aCByZWNvcmQpKQo+ICvCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChhdXRoLW9wdGlvbmFs IChvcGVuc210cGQtaW50ZXJmYWNlLWF1dGgtCj4gb3B0aW9uYWwgcmVjb3JkKSkpCj4gK8KgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc3RyaW5nLWFwcGVuZAo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoaWYgbWFzay1zcmMKPiArwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc3RyaW5nLWFwcGVuZCAibWFz ay1zcmMgIikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCAiIikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHZhcmlhYmxl LT5zdHJpbmcgaG9zdG5hbWUgIzphcHBlbmQgImhvc3RuYW1lICIpCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICh2YXJpYWJsZS0+c3RyaW5nIGhvc3RuYW1lcyAjOmFw cGVuZCAiaG9zdG5hbWVzCj4gPCIgIzpwb3N0cGVuZCAiPiAiKQo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAodmFyaWFibGUtPnN0cmluZyBmaWx0ZXItbmFtZSAjOmFw cGVuZCAiZmlsdGVyCj4gXCIiICM6cG9zdHBlbmQgIlwiICIpCj4gK8KgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgICh2YXJpYWJsZS0+c3RyaW5nIHRhZyAjOmFwcGVuZCAidGFn IFwiIgo+ICM6cG9zdHBlbmQgIlwiICIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgIChpZiBzZWN1cmUtY29ubmVjdGlvbgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChjb25kICgoc3RyaW5nPT8gInNtdHBzIiBzZWN1cmUt Y29ubmVjdGlvbikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgICJzbXRwcyAiKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICgoc3RyaW5nPT8gInRscyIgc2VjdXJl LWNvbm5lY3Rpb24pCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoCAidGxzICIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKChzdHJpbmc9PyAidGxzLXJlcXVpcmUi IHNlY3VyZS0KPiBjb25uZWN0aW9uKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgInRscy1yZXF1aXJlICIpCj4gK8KgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKChzdHJp bmc9PyAidGxzLXJlcXVpcmUtdmVyaWZ5Igo+IHNlY3VyZS1jb25uZWN0aW9uKQo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgInRs cy1yZXF1aXJlIHZlcmlmeSAiKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoCAiIikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgKHZhcmlhYmxlLT5zdHJpbmcgcG9ydCAjOmFwcGVuZCAicG9ydCAiCj4gIzpwb3N0cGVuZCAi ICIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChpZiBwa2kKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAodmFyaWFibGUt PnN0cmluZyAob3BlbnNtdHBkLXBraS1kb21haW4gcGtpKQo+ICM6YXBwZW5kICJwa2kgIikKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAiIikKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGlmIGF1dGgKPiArwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc3RyaW5nLWFwcGVuZCAiYXV0 aCAiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGlmIChvcGVuc210cGQtdGFibGU/IGF1dGgpCj4g K8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc3RyaW5nLWFwcGVuZCAiPCIKPiAob3BlbnNt dHBkLXRhYmxlLW5hbWUgYXV0aCkgIj4gIikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg ICIiKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAi IikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGlmIGF1dGgtb3B0 aW9uYWwKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAo c3RyaW5nLWFwcGVuZCAiYXV0aC1vcHRpb25hbCAiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGlm IChvcGVuc210cGQtdGFibGU/IGF1dGgtCj4gb3B0aW9uYWwpCj4gK8KgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAoc3RyaW5nLWFwcGVuZCAiPCIKPiAob3BlbnNtdHBkLXRhYmxlLW5hbWUgYXV0 aC1vcHRpb25hbCkgIj4gIikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICIiKSkKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAiIikKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIlxuIikpKSkKPiArCj4gKyhkZWZp bmUgKG9wZW5zbXRwZC1zb2NrZXQtPnN0cmluZyByZWNvcmQpCj4gK8KgIChzdHJpbmctYXBwZW5k ICJsaXN0ZW4gb24gc29ja2V0ICIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAg KGxldCogKChmaWx0ZXJzIChvcGVuc210cGQtc29ja2V0LWNvbmZpZ3VyYXRpb24tCj4gZmlsdGVy cyByZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIChmaWx0ZXItbmFtZSAoaWYgZmlsdGVycwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAo aWYgKDwgMSAobGVuZ3RoIGZpbHRlcnMpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIChnZW5lcmF0ZS1maWx0ZXItY2hhaW4tCj4gbmFtZSBmaWx0ZXJzKQo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgIChpZiAob3BlbnNtdHBkLWZpbHRlcj8KPiAoY2FyIGZpbHRl cnMpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRw ZC1maWx0ZXItCj4gbmFtZSAoY2FyIGZpbHRlcnMpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1maWx0ZXItCj4gcGhhc2UtbmFtZSAoY2FyIGZp bHRlcnMpKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICNmKSkKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobWFzay1zcmMgKG9wZW5zbXRwZC1z b2NrZXQtY29uZmlndXJhdGlvbi0KPiBtYXNrLXNyYyByZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICh0YWcgKG9wZW5zbXRwZC1zb2NrZXQt Y29uZmlndXJhdGlvbi10YWcKPiByZWNvcmQpKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIChzdHJpbmctYXBwZW5kCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIChpZiBtYXNrLXNyYwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgIChzdHJpbmctYXBwZW5kICJtYXNrLXNyYyAiKQo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICIiKQo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAodmFyaWFibGUtPnN0cmluZyBmaWx0ZXItbmFt ZSAjOmFwcGVuZCAiZmlsdGVyCj4gXCIiICM6cG9zdHBlbmQgIlwiICIpCj4gK8KgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICh2YXJpYWJsZS0+c3RyaW5nIHRhZyAjOmFwcGVu ZCAidGFnIFwiIgo+ICM6cG9zdHBlbmQgIlwiICIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgICJcbiIpKSkpCj4gKwo+ICsoZGVmaW5lIChvcGVuc210cGQtcmVsYXkt PnN0cmluZyByZWNvcmQpCj4gK8KgIChsZXQgKChiYWNrdXAgKG9wZW5zbXRwZC1yZWxheS1iYWNr dXAgcmVjb3JkKSkKPiArwqDCoMKgwqDCoMKgwqAgKGJhY2t1cC1teCAob3BlbnNtdHBkLXJlbGF5 LWJhY2t1cC1teCByZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDCoCAoaGVsbyAob3BlbnNtdHBkLXJl bGF5LWhlbG8gcmVjb3JkKSkKPiArwqDCoMKgwqDCoMKgwqAgOzsgaGVsby1zcmMgY2FuIGVpdGhl ciBiZSBhIHN0cmluZyBJUCBhZGRyZXNzIG9yIGFuCj4gPG9wZW5zbXRwZC10YWJsZT4KPiArwqDC oMKgwqDCoMKgwqAgKGhlbG8tc3JjIChpZiAob3BlbnNtdHBkLXJlbGF5LWhlbG8tc3JjIHJlY29y ZCkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChpZiAoc3Ry aW5nPyAob3BlbnNtdHBkLXJlbGF5LWhlbG8tc3JjCj4gcmVjb3JkKSkKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1yZWxheS1o ZWxvLXNyYyByZWNvcmQpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIChzdHJpbmctYXBwZW5kICI8XCIiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIChvcGVuc210cGQtdGFibGUtbmFtZQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChv cGVuc210cGQtcmVsYXktc3JjCj4gcmVjb3JkKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAg IlwiPiIpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgI2Yp KQo+ICvCoMKgwqDCoMKgwqDCoCAoZG9tYWluIChpZiAob3BlbnNtdHBkLXJlbGF5LWRvbWFpbiBy ZWNvcmQpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcGVuc210 cGQtdGFibGUtbmFtZQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg IChvcGVuc210cGQtcmVsYXktZG9tYWluIHJlY29yZCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgICNmKSkKPiArwqDCoMKgwqDCoMKgwqAgKGhvc3QgKG9wZW5zbXRw ZC1yZWxheS1ob3N0IHJlY29yZCkpCj4gK8KgwqDCoMKgwqDCoMKgIChuYW1lIChvcGVuc210cGQt cmVsYXktbmFtZSByZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDCoCAocGtpIChpZiAob3BlbnNtdHBk LXJlbGF5LXBraSByZWNvcmQpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChv cGVuc210cGQtcGtpLWRvbWFpbiAob3BlbnNtdHBkLXJlbGF5LXBraSByZWNvcmQpKQo+ICvCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAjZikpCj4gK8KgwqDCoMKgwqDCoMKgIChzcnMg KG9wZW5zbXRwZC1yZWxheS1zcnMgcmVjb3JkKSkKPiArwqDCoMKgwqDCoMKgwqAgKHRscyAob3Bl bnNtdHBkLXJlbGF5LXRscyByZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDCoCAoYXV0aCAoaWYgKG9w ZW5zbXRwZC1yZWxheS1hdXRoIHJlY29yZCkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAob3BlbnNtdHBkLXRhYmxlLW5hbWUKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIChvcGVuc210cGQtcmVsYXktYXV0aCByZWNvcmQpKQo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICNmKSkKPiArwqDCoMKgwqDCoMKgwqAgKG1haWwtZnJv bSAob3BlbnNtdHBkLXJlbGF5LW1haWwtZnJvbSByZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDCoCA7 OyBzcmMgY2FuIGVpdGhlciBiZSBhIHN0cmluZyBJUCBhZGRyZXNzIG9yIGFuIDxvcGVuc210cGQt Cj4gdGFibGU+Cj4gK8KgwqDCoMKgwqDCoMKgIChzcmMgKGlmIChvcGVuc210cGQtcmVsYXktc3Jj IHJlY29yZCkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGlmIChzdHJpbmc/ IChvcGVuc210cGQtcmVsYXktc3JjIHJlY29yZCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1yZWxheS1zcmMgcmVjb3JkKQo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzdHJpbmctYXBwZW5kICI8XCIiCj4g K8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC10YWJsZS1uYW1lCj4gK8KgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAo b3BlbnNtdHBkLXJlbGF5LXNyYyByZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJcIj4iKSkKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgI2YpKSkKPiArwqDCoMKgwqDCoMKgwqAg Cj4gK8KgwqDCoCAoc3RyaW5nLWFwcGVuZAo+ICvCoMKgwqDCoCAiXCIiCj4gK8KgwqDCoMKgIG5h bWUKPiArwqDCoMKgwqAgIlwiICIgInJlbGF5ICIKPiArwqDCoMKgwqAgOztGSVhNRSBzaG91bGQg SSBhbHdheXMgcXVvdGUgdGhlIGhvc3QgZmllbGRuYW1lPyBkbyBJIG5lZWQgdG8KPiBxdW90ZSBs b2NhbGhvc3QgdmlhICJsb2NhbGhvc3QiID8KPiArwqDCoMKgwqAgKHZhcmlhYmxlLT5zdHJpbmcg aG9zdCAjOmFwcGVuZCAiaG9zdCBcIiIgIzpwb3N0cGVuZCAiXCIgIikKPiArwqDCoMKgwqAgKHZh cmlhYmxlLT5zdHJpbmcgYmFja3VwKQo+ICvCoMKgwqDCoCAodmFyaWFibGUtPnN0cmluZyBiYWNr dXAtbXggIzphcHBlbmQgImJhY2t1cCBteCAiKQo+ICvCoMKgwqDCoCAodmFyaWFibGUtPnN0cmlu ZyBoZWxvICM6YXBwZW5kICJoZWxvICIpCj4gK8KgwqDCoMKgICh2YXJpYWJsZS0+c3RyaW5nIGhl bG8tc3JjICM6YXBwZW5kICJoZWxvLXNyYyAiKQo+ICvCoMKgwqDCoCAodmFyaWFibGUtPnN0cmlu ZyBkb21haW4gIzphcHBlbmQgImRvbWFpbiA8XCIiICM6cG9zdHBlbmQgIlwiPgo+ICIpCj4gK8Kg wqDCoMKgICh2YXJpYWJsZS0+c3RyaW5nIGhvc3QgIzphcHBlbmQgImhvc3QgIikKPiArwqDCoMKg wqAgKHZhcmlhYmxlLT5zdHJpbmcgcGtpICM6YXBwZW5kICJwa2kgIikKPiArwqDCoMKgwqAgKHZh cmlhYmxlLT5zdHJpbmcgc3JzKQo+ICvCoMKgwqDCoCAodmFyaWFibGUtPnN0cmluZyB0bHMgIzph cHBlbmQgInRscyAiKQo+ICvCoMKgwqDCoCAodmFyaWFibGUtPnN0cmluZyBhdXRoICM6YXBwZW5k ICJhdXRoIDwiICM6cG9zdHBlbmQgIj4gIikKPiArwqDCoMKgwqAgKHZhcmlhYmxlLT5zdHJpbmcg bWFpbC1mcm9tICM6YXBwZW5kICJtYWlsLWZyb20gIikKPiArwqDCoMKgwqAgKHZhcmlhYmxlLT5z dHJpbmcgc3JjICM6YXBwZW5kICJzcmMgIikKPiArwqDCoMKgwqAgIlxuIikpKQo+ICsKPiArKGRl ZmluZSAob3BlbnNtdHBkLWxtdHAtPnN0cmluZyByZWNvcmQpCj4gK8KgIChzdHJpbmctYXBwZW5k ICJsbXRwICIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1s bXRwLWRlc3RpbmF0aW9uIHJlY29yZCkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgKGlmIChvcGVuc210cGQtbG10cC1yY3B0LXRvIHJlY29yZCkKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoYmVnaW4KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIiAiIChvcGVuc210cGQtbG10cC1yY3B0LXRvIHJlY29y ZCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIiIpKSkKPiAr Cj4gKyhkZWZpbmUgKG9wZW5zbXRwZC1tZGEtPnN0cmluZyByZWNvcmQpCj4gK8KgIChzdHJpbmct YXBwZW5kICJtZGEgIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3BlbnNt dHBkLW1kYS1jb21tYW5kIHJlY29yZCkgIiAiKSkKPiArCj4gKyhkZWZpbmUgKG9wZW5zbXRwZC1t YWlsZGlyLT5zdHJpbmcgcmVjb3JkKQo+ICvCoCAoc3RyaW5nLWFwcGVuZCAibWFpbGRpciAiCj4g K8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJcIiIKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgKGlmIChvcGVuc210cGQtbWFpbGRpci1wYXRobmFtZSByZWNvcmQp Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1t YWlsZGlyLXBhdGhuYW1lIHJlY29yZCkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAifi9NYWlsZGlyIikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgIlwiIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoaWYgKG9wZW5zbXRw ZC1tYWlsZGlyLWp1bmsgcmVjb3JkKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgICIganVuayAiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgIiAiKSkpCj4gKwo+ICsoZGVmaW5lIChvcGVuc210cGQtbG9jYWwtZGVsaXZlcnktPnN0 cmluZyByZWNvcmQpCj4gK8KgIChsZXQgKChuYW1lIChvcGVuc210cGQtbG9jYWwtZGVsaXZlcnkt bmFtZSByZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDCoCAobWV0aG9kIChvcGVuc210cGQtbG9jYWwt ZGVsaXZlcnktbWV0aG9kIHJlY29yZCkpCj4gK8KgwqDCoMKgwqDCoMKgIChhbGlhcyAoaWYgKG9w ZW5zbXRwZC1sb2NhbC1kZWxpdmVyeS1hbGlhcyByZWNvcmQpCj4gK8KgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoCAob3BlbnNtdHBkLXRhYmxlLW5hbWUKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1sb2NhbC1kZWxpdmVyeS1hbGlh cyByZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgI2YpKQo+ ICvCoMKgwqDCoMKgwqDCoCAodHRsIChvcGVuc210cGQtbG9jYWwtZGVsaXZlcnktdHRsIHJlY29y ZCkpCj4gK8KgwqDCoMKgwqDCoMKgICh1c2VyIChvcGVuc210cGQtbG9jYWwtZGVsaXZlcnktdXNl ciByZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDCoCAodXNlcmJhc2UgKGlmIChvcGVuc210cGQtbG9j YWwtZGVsaXZlcnktdXNlcmJhc2UgcmVjb3JkKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC10YWJsZS1uYW1lCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcGVuc210cGQtbG9jYWwtZGVsaXZlcnkt dXNlcmJhc2UgcmVjb3JkKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgICNmKSkKPiArwqDCoMKgwqDCoMKgwqAgKHZpcnR1YWwgKGlmIChvcGVuc210cGQtbG9j YWwtZGVsaXZlcnktdmlydHVhbCByZWNvcmQpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC10YWJsZS1uYW1lCj4gK8KgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3BlbnNtdHBkLWxvY2FsLWRlbGl2ZXJ5LXZpcnR1 YWwgcmVjb3JkKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAj ZikpCj4gK8KgwqDCoMKgwqDCoMKgICh3cmFwcGVyIChvcGVuc210cGQtbG9jYWwtZGVsaXZlcnkt d3JhcHBlciByZWNvcmQpKSkKPiArwqDCoMKgIChzdHJpbmctYXBwZW5kCj4gK8KgwqDCoMKgICJc IiIgbmFtZSAiXCIgIgo+ICvCoMKgwqDCoCAoY29uZCAoKHN0cmluZz8gbWV0aG9kKQo+ICvCoMKg wqDCoMKgwqDCoMKgwqDCoMKgIChzdHJpbmctYXBwZW5kIG1ldGhvZCAiICIpKQo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoCAoKG9wZW5zbXRwZC1tZGE/IG1ldGhvZCkKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAob3BlbnNtdHBkLW1kYS0+c3RyaW5nIG1ldGhvZCkpCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgICgob3BlbnNtdHBkLWxtdHA/IG1ldGhvZCkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCAob3BlbnNtdHBkLWxtdHAtPnN0cmluZyBtZXRob2QpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oCAoKG9wZW5zbXRwZC1tYWlsZGlyPyBtZXRob2QpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqAg KG9wZW5zbXRwZC1tYWlsZGlyLT5zdHJpbmcgbWV0aG9kKSkpCj4gK8KgwqDCoMKgIDs7IEZJWE1F L1RPRE8gc3VwcG9ydCBzcGVjaWZ5aW5nIGFsaWFzIGZpbGU6L3BhdGgvdG8vYWxpYXMtCj4gZmls ZcKgID8KPiArwqDCoMKgwqAgOzsgSSBkbyBub3QgdGhpbmsgdGhhdCBpcyBzb21ldGhpbmcgdGhh dCBJIGNhbiBkby4uLgo+ICvCoMKgwqDCoCAodmFyaWFibGUtPnN0cmluZyBhbGlhcyAjOmFwcGVu ZCAiYWxpYXMgPFwiIiAjOnBvc3RwZW5kICJcIj4gIikKPiArwqDCoMKgwqAgKHZhcmlhYmxlLT5z dHJpbmcgdHRsICM6YXBwZW5kICJ0dGwgIikKPiArwqDCoMKgwqAgKHZhcmlhYmxlLT5zdHJpbmcg dXNlciAjOmFwcGVuZCAidXNlciAiKQo+ICvCoMKgwqDCoCAodmFyaWFibGUtPnN0cmluZyB1c2Vy YmFzZSAjOmFwcGVuZCAidXNlcmJhc2UgPFwiIiAjOnBvc3RwZW5kCj4gIlwiPiAiKQo+ICvCoMKg wqDCoCAodmFyaWFibGUtPnN0cmluZyB2aXJ0dWFsICM6YXBwZW5kICJ2aXJ0dWFsIDwiICM6cG9z dHBlbmQgIj4gIikKPiArwqDCoMKgwqAgKHZhcmlhYmxlLT5zdHJpbmcgd3JhcHBlciAjOmFwcGVu ZCAid3JhcHBlciAiKSkpKQo+ICsKPiArOzsgdGhpcyBmdW5jdGlvbiB0dXJucyBib3RoIG9wZW5z bXRwZC1sb2NhbC1kZWxpdmVyeSBhbmQKPiArOzsgb3BlbnNtdHBkLXJlbGF5IGludG8gc3RyaW5n cy4KPiArKGRlZmluZSAob3BlbnNtdHBkLWFjdGlvbi0+c3RyaW5nIHJlY29yZCkKPiArwqAgKHN0 cmluZy1hcHBlbmQgImFjdGlvbiAiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg IChjb25kICgob3BlbnNtdHBkLWxvY2FsLWRlbGl2ZXJ5PyByZWNvcmQpCj4gK8KgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1sb2NhbC1kZWxp dmVyeS0+c3RyaW5nIHJlY29yZCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgICgob3BlbnNtdHBkLXJlbGF5PyByZWNvcmQpCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1yZWxheS0+c3RyaW5n IHJlY29yZCkpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAiIFxuIikpCj4g Kwo+ICs7OyB0aGlzIHR1cm5zIG9wdGlvbiByZWNvcmRzIGZvdW5kIGluIDxvcGVuc210cGQtbWF0 Y2g+IGludG8KPiBzdHJpbmdzLgo+ICsoZGVmaW5lKiAob3BlbnNtdHBkLW9wdGlvbi0+c3RyaW5n IHJlY29yZAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIzprZXkK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzcGFjZS1hZnRlci0h ICNmKSkKPiArwqAgKGxldCAoKG5vdCAob3BlbnNtdHBkLW9wdGlvbi1ub3QgcmVjb3JkKSkKPiAr wqDCoMKgwqDCoMKgwqAgKG9wdGlvbiAob3BlbnNtdHBkLW9wdGlvbi1vcHRpb24gcmVjb3JkKSkK PiArwqDCoMKgwqDCoMKgwqAgKHJlZ2V4IChvcGVuc210cGQtb3B0aW9uLXJlZ2V4IHJlY29yZCkp Cj4gK8KgwqDCoMKgwqDCoMKgIChkYXRhIChvcGVuc210cGQtb3B0aW9uLWRhdGEgcmVjb3JkKSkp Cj4gK8KgwqDCoCAoc3RyaW5nLWFwcGVuZAo+ICvCoMKgwqDCoCAoaWYgbm90Cj4gK8KgwqDCoMKg wqDCoMKgwqAgKGlmIHNwYWNlLWFmdGVyLSEKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICIh ICIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICIhIikKPiArwqDCoMKgwqDCoMKgwqDCoCAi IikKPiArwqDCoMKgwqAgb3B0aW9uICIgIgo+ICvCoMKgwqDCoCAoaWYgcmVnZXgKPiArwqDCoMKg wqDCoMKgwqDCoCAicmVnZXggIgo+ICvCoMKgwqDCoMKgwqDCoMKgICIiKQo+ICvCoMKgwqDCoCAo aWYgZGF0YQo+ICvCoMKgwqDCoMKgwqDCoMKgIChpZiAob3BlbnNtdHBkLXRhYmxlPyBkYXRhKQo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHN0cmluZy1hcHBlbmQgIjwiIChvcGVuc210cGQt dGFibGUtbmFtZSBkYXRhKSAiPiAiKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHN0cmlu Zy1hcHBlbmQgZGF0YSAiICIpKQo+ICvCoMKgwqDCoMKgwqDCoMKgICIiKSkpKQo+ICsKPiArKGRl ZmluZSAob3BlbnNtdHBkLW1hdGNoLT5zdHJpbmcgcmVjb3JkKQo+ICvCoCAoc3RyaW5nLWFwcGVu ZCAibWF0Y2ggIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGV0KiAoKGFj dGlvbiAob3BlbnNtdHBkLW1hdGNoLWFjdGlvbiByZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChuYW1lIChjb25kIFsob3BlbnNtdHBkLXJl bGF5PyBhY3Rpb24pCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3BlbnNtdHBkLXJlbGF5LW5hbWUgYWN0 aW9uKV0KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBbKG9wZW5zbXRwZC1sb2NhbC1kZWxpdmVyeT8KPiBhY3Rp b24pCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3BlbnNtdHBkLWxvY2FsLWRlbGl2ZXJ5LW5hbWUKPiBh Y3Rpb24pXQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIFtlbHNlICdyZWplY3RdKSkKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3B0aW9ucyAob3BlbnNtdHBkLW1h dGNoLW9wdGlvbnMgcmVjb3JkKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCAoc3RyaW5nLWFwcGVuZAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCAoaWYgb3B0aW9ucwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgIChhcHBseSBzdHJpbmctYXBwZW5kCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobWFwIG9wZW5zbXRwZC1vcHRp b24tPnN0cmluZwo+IG9wdGlvbnMpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgICIiKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCAoaWYgKHN0cmluZz8gbmFtZSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoCAoc3RyaW5nLWFwcGVuZCAiYWN0aW9uICIgIlwiIiBuYW1lICJcIiAi KQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJyZWpl Y3QgIikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIlxuIikpKSkK PiArCj4gKyhkZWZpbmUgKG9wZW5zbXRwZC1jYS0+c3RyaW5nIHJlY29yZCkKPiArwqAgKHN0cmlu Zy1hcHBlbmQgImNhICIgKG9wZW5zbXRwZC1jYS1uYW1lIHJlY29yZCkgIiAiCj4gK8KgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJjZXJ0IFwiIiAob3BlbnNtdHBkLWNhLWZpbGUgcmVj b3JkKSAiXCJcbiIpKQo+ICsKPiArKGRlZmluZSAob3BlbnNtdHBkLXBraS0+c3RyaW5nIHJlY29y ZCkKPiArwqAgKGxldCAoKGRvbWFpbiAob3BlbnNtdHBkLXBraS1kb21haW4gcmVjb3JkKSkKPiAr wqDCoMKgwqDCoMKgwqAgKGNlcnQgKG9wZW5zbXRwZC1wa2ktY2VydCByZWNvcmQpKQo+ICvCoMKg wqDCoMKgwqDCoCAoa2V5IChvcGVuc210cGQtcGtpLWtleSByZWNvcmQpKQo+ICvCoMKgwqDCoMKg wqDCoCAoZGhlIChvcGVuc210cGQtcGtpLWRoZSByZWNvcmQpKSkKPiArwqDCoMKgIChzdHJpbmct YXBwZW5kICJwa2kgIiBkb21haW4gIiAiICJjZXJ0IFwiIiBjZXJ0ICJcIiBcbiIKPiArwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJwa2kgIiBkb21haW4gIiAiICJrZXkgXCIi IGtleSAiXCIgXG4iCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoaWYg ZGhlCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzdHJp bmctYXBwZW5kCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgInBraSAiIGRvbWFpbiAiICIgImRoZSAiIGRoZSAiXG4iKQo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAiIikpKSkKPiArCj4gKyhkZWZpbmUgKGdlbmVy YXRlLWZpbHRlci1jaGFpbi1uYW1lIGxpc3Qtb2YtZmlsdGVycykKPiArwqAgKHN0cmluZy1kcm9w LXJpZ2h0IChhcHBseSBzdHJpbmctYXBwZW5kCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZmxhdHRlbgo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobWFwIChsYW1iZGEgKGZp bHRlcikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGlmIChvcGVu c210cGQtZmlsdGVyPyBmaWx0ZXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcGVuc210 cGQtZmlsdGVyLW5hbWUKPiBmaWx0ZXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcGVu c210cGQtZmlsdGVyLXBoYXNlLQo+IG5hbWUgZmlsdGVyKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICIt IikpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoCBsaXN0LW9mLWZpbHRlcnMpKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAxKSkKPiArCj4gKzs7IHRoaXMgcHJvY2VkdXJlIHRha2Vz IGluIGEgbGlzdCBvZiA8b3BlbnNtdHBkLWZpbHRlcj4gYW5kCj4gPG9wZW5zbXRwZC1maWx0ZXIt cGhhc2U+LAo+ICs7OyByZXR1cm5zIGEgc3RyaW5nIG9mIHRoZSBmb3JtOgo+ICs7OyBmaWx0ZXIg InVuaXF1ZWx5R2VuZXJhdGVkTmFtZSIgY2hhaW4gY2hhaW4geyAiZmlsdGVyLW5hbWUiLAo+ICJm aWx0ZXItbmFtZTIiIFssIC4uLl19Cj4gKyhkZWZpbmUgKG9wZW5zbXRwZC1maWx0ZXItY2hhaW4t PnN0cmluZyBsaXN0LW9mLWZpbHRlcnMpCj4gK8KgIChzdHJpbmctYXBwZW5kICJmaWx0ZXIgXCIi Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChnZW5lcmF0ZS1maWx0ZXItY2hh aW4tbmFtZSBsaXN0LW9mLWZpbHRlcnMpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgICJcIiAiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJjaGFpbiB7Igo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc3RyaW5nLWRyb3AtcmlnaHQKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoYXBwbHkgc3RyaW5nLWFwcGVuZAo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGZsYXR0 ZW4KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAg KG1hcCAobGFtYmRhIChmaWx0ZXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3QKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJcIiIK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIChpZiAob3BlbnNtdHBkLWZpbHRlcj8gZmlsdGVyKQo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAob3BlbnNtdHBkLWZpbHRlci1uYW1lIGZpbHRlcikKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgKG9wZW5zbXRwZC1maWx0ZXItcGhhc2UtbmFtZQo+IGZpbHRlcikpCj4gK8KgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAi XCIsICIpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgbGlzdC1vZi1maWx0ZXJzKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgMikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIn1c biIpKQo+ICsKPiArKGRlZmluZSAob3BlbnNtdHBkLWZpbHRlci1waGFzZS0+c3RyaW5nIHJlY29y ZCkKPiArwqAgKGxldCAoKG5hbWUgKG9wZW5zbXRwZC1maWx0ZXItcGhhc2UtbmFtZSByZWNvcmQp KQo+ICvCoMKgwqDCoMKgwqDCoCAocGhhc2UgKG9wZW5zbXRwZC1maWx0ZXItcGhhc2UtcGhhc2Ug cmVjb3JkKSkKPiArwqDCoMKgwqDCoMKgwqAgKGRlY2lzaW9uIChvcGVuc210cGQtZmlsdGVyLXBo YXNlLWRlY2lzaW9uIHJlY29yZCkpCj4gK8KgwqDCoMKgwqDCoMKgIChvcHRpb25zIChvcGVuc210 cGQtZmlsdGVyLXBoYXNlLW9wdGlvbnMgcmVjb3JkKSkKPiArwqDCoMKgwqDCoMKgwqAgKG1lc3Nh Z2UgKG9wZW5zbXRwZC1maWx0ZXItcGhhc2UtbWVzc2FnZSByZWNvcmQpKQo+ICvCoMKgwqDCoMKg wqDCoCAodmFsdWUgKG9wZW5zbXRwZC1maWx0ZXItcGhhc2UtdmFsdWUgcmVjb3JkKSkpCj4gK8Kg wqDCoCAoc3RyaW5nLWFwcGVuZCAiZmlsdGVyICIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgICJcIiIgbmFtZSAiXCIgIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgInBoYXNlICIgcGhhc2UgIiAiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoCAibWF0Y2ggIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKGFwcGx5IHN0cmluZy1hcHBlbmQgOyB0dXJuIHRoZSBvcHRpb25zIGludG8gYQo+IHN0 cmluZwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCAoZmxhdHRlbgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgIChtYXAgKGxhbWJkYSAob3B0aW9uKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1v cHRpb24tPnN0cmluZyBvcHRpb24KPiAjOnNwYWNlLWFmdGVyLSEgI2YpKQo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCBvcHRp b25zKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAiICIKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIGRlY2lzaW9uICIgIgo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGlmIChzdHJpbmctaW4tbGlzdD8gZGVjaXNp b24gKGxpc3QgInJlamVjdCIKPiAiZGlzY29ubmVjdCIpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoc3RyaW5nLWFwcGVuZCAiXCIiIG1lc3NhZ2UgIlwi IikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIiIpCj4g K8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoaWYgKHN0cmluZz0/ICJyZXdy aXRlIiBkZWNpc2lvbikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKHN0cmluZy1hcHBlbmQgInJld3JpdGUgIiAobnVtYmVyLT5zdHJpbmcKPiB2YWx1ZSkp Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICIiKQo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIlxuIikpKQo+ICsKPiArOzsgZmls dGVycyBlbGVtZW50cyBtYXkgYmUgPG9wZW5zbXRwZC1maWx0ZXI+LCA8b3BlbnNtdHBkLWZpbHRl ci0KPiBwaGFzZT4sCj4gKzs7IGFuZCBsaXN0cyB0aGF0IGxvb2sgbGlrZSAobGlzdCAob3BlbnNt dHBkLWZpbHRlci4uLikgKG9wZW5zbXRwZC0KPiBmaWx0ZXItcGhhc2UgLi4uKQo+ICs7OyAuLi4p Cj4gKzs7IHRoaXMgZnVuY3Rpb24gY29udmVydHMgaXQgdG8gYSBzdHJpbmcuCj4gKzs7IENvbnNp ZGVyIGlmIGEgdXNlciBwYXNzZWQgaW4gYSB2YWxpZCA8b3BlbnNtdHBkLWNvbmZpZ3VyYXRpb24+ LAo+IHdob3NlIHRvdGFsIHZhbGlkIGZpbHRlcnMKPiArOzsgc28gdGhhdCAoZ2V0LW9wZW5zbXRw ZC1maWx0ZXJzIChvcGVuc210cGQtY29uZmlndXJhdGlvbikpIHJldHVybnMKPiArOzsgbG9vayBs aWtlIHRoaXM6ICh3ZSB3aWxsIGNhbGwgdGhpcyBsaXN0ICJ0b3RhbCBmaWx0ZXJzIik6Cj4gKzs7 IChsaXN0IChvcGVuc210cGQtZmlsdGVyCj4gKzs7wqDCoMKgwqDCoMKgwqDCoCAobmFtZSAicnNw YW1kIikKPiArOzvCoMKgwqDCoMKgwqDCoMKgIChwcm9jICJyc3BhbWQiKSkKPiArOzvCoMKgwqDC oMKgwqAgKGxpc3QgKG9wZW5zbXRwZC1maWx0ZXItcGhhc2UgOyB0aGlzIGlzIGEgbGlzdGVuLW9u LCB3aXRoIGEKPiBmaWx0ZXItY2hhaW4uCj4gKzs7wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCAobmFtZSAiZGtpbXNpZ24iKQo+ICs7O8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgLi4u KQo+ICs7O8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1maWx0ZXIKPiAr OzvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobmFtZSAicnNwYW1kIikKPiArOzvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAocHJvYyAicnNwYW1kIikpKSkKPiArOzsK PiArOzsgZGlkIHlvdSBub3RpY2UgdGhhdCBmaWx0ZXIgInJzcGFtZCIgaXMgbGlzdGVkIHR3aWNl P8KgIEhvdyBkbyB5b3UKPiBtYWtlIHN1cmUgdGhhdCBpdCBpcyBOT1QKPiArOzsgcHJpbnRlZCB0 d2ljZSBpbiBzbXRwZC5jb25mPwo+ICs7OyAxc3QgZmxhdHRlbiAidG90YWwgZmlsdGVycyIsIHRo ZW4gcmVtb3ZlIGl0cyBkdXBsaWNhdGVzLsKgIFRoZW4KPiBwcmludCBhbGwgb2YgdGhvc2UgZmls dGVycy4KPiArOzsgMm5kIG5vdyB3ZSBnbyB0aHJvdWdoICJ0b3RhbCBmaWx0ZXJzIiwgYW5kIHdl IG9ubHkgcHJpbnQgdGhlIG5vbi0KPiBmaWx0ZXItY2hhaW5zLgo+ICsoZGVmaW5lIChvcGVuc210 cGQtZmlsdGVycy0+bGlzdC1vZi1zdHJpbmdzLWFuZC1nZXhwcyBmaWx0ZXJzKQo+ICvCoCA7OyBm aXJzdCBkaXNwbGF5IHRoZSB1bmlxdWUgPG9wZW5zbXRwZC1maWx0ZXI+cy4gYW5kIDxvcGVuc210 cGQtCj4gZmlsdGVyLXBoYXNlPnMuCj4gK8KgIDs7IHRvIGRvIHRoaXM6IGZsYXR0ZW4gZmlsdGVy cywgdGhlbiByZW1vdmUgZHVwbGljYXRlcy4KPiArwqAgKGxpc3QKPiArwqDCoCAoYXBwbHkgc3Ry aW5nLWFwcGVuZAo+ICvCoMKgwqDCoMKgwqDCoMKgwqAgKG1hcCAobGFtYmRhIChmaWx0ZXIpCj4g K8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChpZiAob3BlbnNtdHBkLWZpbHRlci1w aGFzZT8gZmlsdGVyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg IChvcGVuc210cGQtZmlsdGVyLXBoYXNlLT5zdHJpbmcgZmlsdGVyKQo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICIiKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAoZGVsZXRlLWR1cGxpY2F0ZXMgKGZsYXR0ZW4gZmlsdGVycykpKSkKPiArwqDCoCA7 OyBwcmludCBvdXQgdGhlIGZpbHRlci1jb25maWd1cmF0aW9ucwo+ICvCoMKgIDs7IHdvdWxkIHZh bHVlcyBhbmQgb3IgY2FsbC13aXRoLXZhbHVlcyBhbmQgb3IgcmVjaWV2ZSB3b3JrIGhlcmU/Cj4g K8KgwqAgKGxpc3QgKG1hcCAobGFtYmRhIChmaWx0ZXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAoaWYgKG9wZW5zbXRwZC1maWx0ZXI/IGZpbHRlcikKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3QgImZpbHRlciAiCj4gK8KgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJcIiIgKG9wZW5zbXRwZC1m aWx0ZXItbmFtZSBmaWx0ZXIpICJcIiAiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgIChpZiAob3BlbnNtdHBkLWZpbHRlci1leGVjIGZpbHRlcikK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCAicHJvYy1leGVjICIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAicHJvYyAiKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAiXCIiIChvcGVuc210cGQtZmlsdGVyLXByb2Mg ZmlsdGVyKSAiXCIiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgICJcblxuIikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgIiIpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZGVsZXRlLWR1cGxpY2F0ZXMg KGZsYXR0ZW4gZmlsdGVycykpKSkKPiArwqDCoCA7OyBub3cgd2UgaGF2ZSB0byBwcmludCB0aGUg ZmlsdGVyIGNoYWlucy4KPiArwqDCoCAoYXBwbHkgc3RyaW5nLWFwcGVuZAo+ICvCoMKgwqDCoMKg wqDCoMKgwqAgKG1hcCAobGFtYmRhIChmaWx0ZXIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgIChjb25kICgobGlzdD8gZmlsdGVyKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcGVuc210cGQtZmlsdGVyLWNoYWluLT5zdHJpbmcg ZmlsdGVyKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAg KGVsc2XCoMKgwqDCoMKgIDsgeW91IGFyZSBhIDxvcGVuc210cGQtZmlsdGVyPgo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICIiKSkpCj4gK8KgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqAgZmlsdGVycykpKSkKPiArCj4gKyhkZWZpbmUgKG9wZW5zbXRw ZC1jb25maWd1cmF0aW9uLWxpc3Rlbi0+c3RyaW5nIHN0cmluZykKPiArwqAgKHN0cmluZy1hcHBl bmQKPiArwqDCoCAiaW5jbHVkZSBcIiIgc3RyaW5nICJcIlxuIikpCj4gKwo+ICsoZGVmaW5lIChv cGVuc210cGQtY29uZmlndXJhdGlvbi1zcnMtPnN0cmluZyByZWNvcmQpCj4gK8KgIChsZXQgKChr ZXkgKG9wZW5zbXRwZC1zcnMta2V5IHJlY29yZCkpCj4gK8KgwqDCoMKgwqDCoMKgIChiYWNrdXAt a2V5IChvcGVuc210cGQtc3JzLWJhY2t1cC1rZXkgcmVjb3JkKSkKPiArwqDCoMKgwqDCoMKgwqAg KHR0bC1kZWxheSAob3BlbnNtdHBkLXNycy10dGwtZGVsYXkgcmVjb3JkKSkpCj4gK8KgwqDCoCAo c3RyaW5nLWFwcGVuZAo+ICvCoMKgwqDCoCAodmFyaWFibGUtPnN0cmluZyBrZXkgIzphcHBlbmQg InNycyBrZXkgIiAjOnBvc3RwZW5kICJcbiIpCj4gK8KgwqDCoMKgICh2YXJpYWJsZS0+c3RyaW5n IGJhY2t1cC1rZXkgIzphcHBlbmQgInNycyBrZXkgYmFja3VwICIKPiAjOnBvc3RwZW5kICJcbiIp Cj4gK8KgwqDCoMKgICh2YXJpYWJsZS0+c3RyaW5nIHR0bC1kZWxheSAjOmFwcGVuZCAic3JzIHR0 bCAiICM6cG9zdHBlbmQKPiAiXG4iKQo+ICvCoMKgwqDCoCAiXG4iKSkpCj4gKwo+ICs7OyBUT0RP IG1ha2Ugc3VyZSBhbGwgb3B0aW9ucyBoZXJlIHdvcmshwqAgSSBqdXN0IGZpeGVkIGxpbWl0LW1h eC0KPiByY3B0IQo+ICsoZGVmaW5lIChvcGVuc210cGQtc210cC0+c3RyaW5nIHJlY29yZCkKPiAr wqAgKGxldCAoKGNpcGhlcnMgKG9wZW5zbXRwZC1zbXRwLWNpcGhlcnMgcmVjb3JkKSkKPiArwqDC oMKgwqDCoMKgwqAgKGxpbWl0LW1heC1tYWlscyAob3BlbnNtdHBkLXNtdHAtbGltaXQtbWF4LW1h aWxzIHJlY29yZCkpCj4gK8KgwqDCoMKgwqDCoMKgIChsaW1pdC1tYXgtcmNwdCAob3BlbnNtdHBk LXNtdHAtbGltaXQtbWF4LXJjcHQgcmVjb3JkKSkKPiArwqDCoMKgwqDCoMKgwqAgKG1heC1tZXNz YWdlLXNpemUgKG9wZW5zbXRwZC1zbXRwLW1heC1tZXNzYWdlLXNpemUgcmVjb3JkKSkKPiArwqDC oMKgwqDCoMKgwqAgKHN1Yi1hZGRyLWRlbGltIChvcGVuc210cGQtc210cC1zdWItYWRkci1kZWxp bSByZWNvcmQpKSkKPiArwqDCoMKgIChzdHJpbmctYXBwZW5kCj4gK8KgwqDCoMKgICh2YXJpYWJs ZS0+c3RyaW5nIGNpcGhlcnMgIzphcHBlbmQgInNtdHAgY2lwaGVycyAiICM6cG9zdHBlbmQKPiAi XG4iKQo+ICvCoMKgwqDCoCAodmFyaWFibGUtPnN0cmluZyBsaW1pdC1tYXgtbWFpbHMgIzphcHBl bmQgInNtdHAgbGltaXQgbWF4LQo+IG1haWxzICIgIzpwb3N0cGVuZCAiXG4iKQo+ICvCoMKgwqDC oCAodmFyaWFibGUtPnN0cmluZyBsaW1pdC1tYXgtcmNwdCAjOmFwcGVuZCAic210cCBsaW1pdCBt YXgtcmNwdAo+ICIgIzpwb3N0cGVuZCAiXG4iKQo+ICvCoMKgwqDCoCAodmFyaWFibGUtPnN0cmlu ZyBtYXgtbWVzc2FnZS1zaXplICM6YXBwZW5kICJzbXRwIG1heC1tZXNzYWdlLQo+IHNpemUgIiAj OnBvc3RwZW5kICJcbiIpCj4gK8KgwqDCoMKgICh2YXJpYWJsZS0+c3RyaW5nIHN1Yi1hZGRyLWRl bGltICM6YXBwZW5kICJzbXRwIHN1Yi1hZGRyLWRlbGltCj4gIiAjOnBvc3RwZW5kICJcbiIpCj4g K8KgwqDCoMKgICJcbiIpKSkKPiArCj4gKyhkZWZpbmUgKG9wZW5zbXRwZC1jb25maWd1cmF0aW9u LXF1ZXVlLT5zdHJpbmcgcmVjb3JkKQo+ICvCoCAobGV0ICgoY29tcHJlc3Npb24gKG9wZW5zbXRw ZC1xdWV1ZS1jb21wcmVzc2lvbiByZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDCoCAoZW5jcnlwdGlv biAob3BlbnNtdHBkLXF1ZXVlLWVuY3J5cHRpb24gcmVjb3JkKSkKPiArwqDCoMKgwqDCoMKgwqAg KHR0bC1kZWxheSAob3BlbnNtdHBkLXF1ZXVlLXR0bC1kZWxheSByZWNvcmQpKSkKPiArwqDCoMKg IChzdHJpbmctYXBwZW5kCj4gK8KgwqDCoMKgIChpZiBjb21wcmVzc2lvbgo+ICvCoMKgwqDCoMKg wqDCoMKgICJxdWV1ZSBjb21wcmVzc2lvblxuIgo+ICvCoMKgwqDCoMKgwqDCoMKgICIiKQo+ICvC oMKgwqDCoCAoaWYgZW5jcnlwdGlvbgo+ICvCoMKgwqDCoMKgwqDCoMKgIChzdHJpbmctYXBwZW5k Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoCAicXVldWUgZW5jcnlwdGlvbiAiCj4gK8KgwqDCoMKgwqDC oMKgwqDCoCAoaWYgKG5vdCAoYm9vbGVhbj8gZW5jcnlwdGlvbikpCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIGVuY3J5cHRpb24KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIiIp Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoCAiXG4iKQo+ICvCoMKgwqDCoMKgwqDCoMKgICIiKQo+ICvC oMKgwqDCoCAoaWYgdHRsLWRlbGF5Cj4gK8KgwqDCoMKgwqDCoMKgwqAgKHN0cmluZy1hcHBlbmQg InF1ZXVlIHR0bCIgdHRsLWRlbGF5ICJcbiIpCj4gK8KgwqDCoMKgwqDCoMKgwqAgIiIpKSkpCj4g Kwo+ICs7OyBidWlsZCBhIGxpc3Qgb2YgPG9wZW5zbXRwZC1hY3Rpb24+IGZyb20KPiArOzsgb3Bl bnNtdHBkLWNvbmZpZ3VyYXRpb24tbWF0Y2hlcywgd2hpY2ggaXMgYSBsaXN0IG9mIDxvcGVuc210 cGQtCj4gbWF0Y2g+Lgo+ICs7OyBFYWNoIDxvcGVuc210cGQtbWF0Y2g+IGhhcyBhIGZpZWxkbmFt ZSAnYWN0aW9uJywgd2hpY2ggYWNjZXB0cyBhbgo+IDxvcGVuc210cGQtYWN0aW9uPi4KPiArKGRl ZmluZSAoZ2V0LW9wZW5zbXRwZC1hY3Rpb25zIHJlY29yZCkKPiArwqAgKGRlZmluZSBvcGVuc210 cGQtYWN0aW9ucwo+ICvCoMKgwqAgKGxldCBsb29wICgobGlzdCAob3BlbnNtdHBkLWNvbmZpZ3Vy YXRpb24tbWF0Y2hlcyByZWNvcmQpKSkKPiArwqDCoMKgwqDCoCAoaWYgKG51bGw/IGxpc3QpCj4g K8KgwqDCoMKgwqDCoMKgwqDCoCAnKCkKPiArwqDCoMKgwqDCoMKgwqDCoMKgIChjb25zIChvcGVu c210cGQtbWF0Y2gtYWN0aW9uIChjYXIgbGlzdCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAobG9vcCAoY2RyIGxpc3QpKSkpKSkKPiArwqAgKGRlbGV0ZS1kdXBsaWNhdGVzIChh cHBlbmQgb3BlbnNtdHBkLWFjdGlvbnMpKSkKPiArCj4gKzs7IGJ1aWxkIGEgbGlzdCBvZiBvcGVu c210cGQtcGtpcyBmcm9tCj4gKzs7IG9wZW5zbXRwZC1jb25maWd1cmF0aW9uLWxpc3Rlbi1vbnMg YW5kCj4gKzs7IGdldC1vcGVuc210cGQtYWN0aW9ucwo+ICsoZGVmaW5lIChnZXQtb3BlbnNtdHBk LXBraXMgcmVjb3JkKQo+ICvCoCA7OyBUT0RPL0ZJWE1FL21heWJlL3dpc2hsaXN0IGNvdWxkIGdl dC1vcGVuc210cGQtYWN0aW9ucyAtPiBOT1QKPiBoYXZlIGFuIG9wZW5zbXRwZC1yZWxheT8KPiAr wqAgOzsgSSB0aGluayBzby7CoCBBbmQgaWYgaXQgZGlkIE5PVCBoYXZlIGEgcmVsYXkgY29uZmln dXJhdGlvbiwgdGhlbgo+IGFjdGlvbi1wa2lzIHdvdWxkIGJlICcoKSB3aGVuCj4gK8KgIDs7IGl0 IG5lZWRzIHRvIGJlICNmLsKgIGJlY2F1c2UgaWYgdGhlIG9wZW5zbXRwZC1jb25maWd1cmF0aW9u IGhhcwo+IE5PIHBraXMsIHRoZW4gdGhpcyBmdW5jdGlvbiB3aWxsCj4gK8KgIDs7IHJldHVybiAn KCksIHdoZW4gaXQgc2hvdWxkIHJldHVybiAjZi7CoCBJZiBpdCByZXR1cm5zICcoKSwgdGhlbgo+ IG9wZW5zbXRwZC1jb25maWd1cmF0aW9uLWZpZWxkbmFtZS0+c3RyaW5nIHdpbGwKPiArwqAgOzsg cHJpbnQgdGhlIHN0cmluZyAiXG4iIGluc3RlYWQgb2YgIiIKPiArwqAgKGRlZmluZSBhY3Rpb24t cGtpcwo+ICvCoMKgwqAgKGxldCBsb29wMSAoKGxpc3QgKGdldC1vcGVuc210cGQtYWN0aW9ucyBy ZWNvcmQpKSkKPiArwqDCoMKgwqDCoCAoaWYgKG51bGw/IGxpc3QpCj4gK8KgwqDCoMKgwqDCoMKg wqDCoCAnKCkKPiArwqDCoMKgwqDCoMKgwqDCoMKgIChpZiAoYW5kIChvcGVuc210cGQtcmVsYXk/ IChjYXIgbGlzdCkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3Bl bnNtdHBkLXJlbGF5LXBraSAoY2FyIGxpc3QpKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgKGNvbnMgKG9wZW5zbXRwZC1yZWxheS1wa2kgKGNhciBsaXN0KSkKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxvb3AxIChjZHIgbGlzdCkpKQo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoCAobG9vcDEgKGNkciBsaXN0KSkpKSkpCj4gK8KgIDs7IEZJWE1F L1RPRE8vbWF5YmUvd2lzaGxpc3QKPiArwqAgOzsgdGhpcyBjb3VsZCBiZSAjZiBha2EgbGVmdCBi bGFuay4gYWthIHRoZXJlIGFyZSBubyBsaXN0ZW4tb25zCj4gcmVjb3JkcyB3aXRoIHBraXMuCj4g K8KgIDs7IGFrYSB0aGVyZSBhcmUgbm8gbGluZXMgaW4gdGhlIGNvbmZpZ3VyYXRpb24gbGlrZToK PiArwqAgOzsgbGlzdGVuIG9uIGV0aDAgdGxzIHBraSBzbXRwLmdudWNvZGUubWUgaW4gdGhhdCBj YXNlIHRoZQo+IHNtdHBkLmNvbmYgd2lsbCBoYXZlIGFuIGV4dHJhICJcbiIKPiArwqAgKGRlZmlu ZSBsaXN0ZW4tb24tcGtpcwo+ICvCoMKgwqAgKGxldCBsb29wMiAoKGxpc3QgKG9wZW5zbXRwZC1j b25maWd1cmF0aW9uLWxpc3Rlbi1vbnMgcmVjb3JkKSkpCj4gK8KgwqDCoMKgwqAgKGlmIChudWxs PyBsaXN0KQo+ICvCoMKgwqDCoMKgwqDCoMKgwqAgJygpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoCAo aWYgKG9wZW5zbXRwZC1pbnRlcmZhY2UtcGtpIChjYXIgbGlzdCkpCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIChjb25zIChvcGVuc210cGQtaW50ZXJmYWNlLXBraSAoY2FyIGxpc3QpKQo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobG9vcDIgKGNkciBsaXN0 KSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsb29wMiAoY2RyIGxpc3QpKSkpKSkK PiArwqAgKGRlbGV0ZS1kdXBsaWNhdGVzIChhcHBlbmQgYWN0aW9uLXBraXMgbGlzdGVuLW9uLXBr aXMpKSkKPiArCj4gKzs7IHRha2VzIGluIGEgPG9wZW5zbXRwZC1jb25maWd1cmF0aW9uPiBhbmQg cmV0dXJucyBhIGxpc3Qgd2hvc2UKPiBlbGVtZW50cyBhcmUgPG9wZW5zbXRwZC1maWx0ZXI+LAo+ ICs7OyA8b3BlbnNtdHBkLWZpbHRlci1waGFzZT4sIGFuZCBhIGZpbHRlci1jaGFpbi4KPiArOzsg SXQgcmV0dXJucyBhIGxpc3Qgb2YgPG9wZW5zbXRwZC1maWx0ZXI+IGFuZC9vciA8b3BlbnNtdHBk LWZpbHRlci0KPiBwaGFzZT4KPiArOzsgaGVyZSdzIGFuIGV4YW1wbGUgb2Ygd2hhdCB0aGlzIHBy b2NlZHVyZSBtaWdodCByZXR1cm46Cj4gKzs7IChsaXN0IChvcGVuc210cGQtZmlsdGVyLi4uKSAo b3BlbnNtdHBkLWZpbHRlci1waGFzZSAuLi4pCj4gKzs7wqDCoMKgwqDCoMKgIChvcGVubXN0cGQt ZmlsdGVyIC4uLikgKG9wZW5zbXRwZC1maWx0ZXItcGhhc2UgLi4uKQo+ICs7O8KgwqDCoMKgwqDC oCA7OyB0aGlzIG5leHQgbGlzdCBpcyBhIGZpbHRlci1jaGFpbi4KPiArOzvCoMKgwqDCoMKgwqAg KGxpc3QgKG9wZW5zbXRwZC1maWx0ZXItcGhhc2UgLi4uKSAob3BlbnNtdHBkLWZpbHRlci4uLikp KQo+ICs7Owo+ICs7OyBUaGlzIHByb2NlZHVyZSBoYW5kbGVzIGZpbHRlciBjaGFpbnMgYSBsaXR0 bGUgb2RkLgo+ICsoZGVmaW5lIChnZXQtb3BlbnNtdHBkLWZpbHRlcnMgcmVjb3JkKQo+ICvCoCAo ZGVmaW5lIGxpc3Qtb2YtbGlzdGVuLW9uLXJlY29yZHMgKGlmIChvcGVuc210cGQtY29uZmlndXJh dGlvbi0KPiBsaXN0ZW4tb25zIHJlY29yZCkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcGVu c210cGQtY29uZmlndXJhdGlvbi0KPiBsaXN0ZW4tb25zIHJlY29yZCkKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgICcoKSkpCj4gKwo+ICvCoCAoZGVmaW5lIGxpc3Rlbi1vbi1zb2NrZXQtZmlsdGVy cwo+ICvCoMKgwqAgKGlmIChvcGVuc210cGQtc29ja2V0LWNvbmZpZ3VyYXRpb24tZmlsdGVycyAo b3BlbnNtdHBkLQo+IGNvbmZpZ3VyYXRpb24tbGlzdGVuLW9uLXNvY2tldCByZWNvcmQpKQo+ICvC oMKgwqDCoMKgwqDCoCAob3BlbnNtdHBkLXNvY2tldC1jb25maWd1cmF0aW9uLWZpbHRlcnMgKG9w ZW5zbXRwZC0KPiBjb25maWd1cmF0aW9uLWxpc3Rlbi1vbi1zb2NrZXQgcmVjb3JkKSkKPiArwqDC oMKgwqDCoMKgwqAgJygpKSkKPiArCj4gK8KgIChkZWxldGUtZHVwbGljYXRlcwo+ICvCoMKgIChh cHBlbmQgKHJlbW92ZSBib29sZWFuPwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKG1hcC1pbi1vcmRlciAobGFtYmRhIChsaXN0ZW4tb24tcmVjb3JkKSA7IGdldAo+IHRo ZSBmaWx0ZXJzIGZvdW5kIGluIHRoZSA8bGlzdGVuLW9uLXJlY29yZD5zCj4gK8KgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg IChpZiAoYW5kIChvcGVuc210cGQtaW50ZXJmYWNlLQo+IGZpbHRlcnMgbGlzdGVuLW9uLXJlY29y ZCkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKD0gMSAobGVuZ3RoIChvcGVuc210 cGQtCj4gaW50ZXJmYWNlLWZpbHRlcnMKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIGxpc3Rlbi1vbi0KPiByZWNvcmQpKSkpCj4gK8Kg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgKGNhciAob3BlbnNtdHBkLWludGVyZmFjZS0KPiBmaWx0ZXJzIGxp c3Rlbi1vbi1yZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcGVuc210cGQtaW50ZXJm YWNlLWZpbHRlcnMKPiBsaXN0ZW4tb24tcmVjb3JkKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgbGlzdC1vZi1saXN0 ZW4tb24tcmVjb3JkcykpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgIGxpc3Rlbi1vbi1zb2NrZXQt ZmlsdGVycykpKQo+ICsKPiArKGRlZmluZSAoZmxhdHRlbiAuIGxzdCkKPiArwqAgIlJldHVybiBh IGxpc3QgdGhhdCByZWN1cnNpdmVseSBjb25jYXRlbmF0ZXMgYWxsIHN1Yi1saXN0cyBvZgo+IExT VC4iCj4gK8KgIChkZWZpbmUgKGZsYXR0ZW4xIGhlYWQgb3V0KQo+ICvCoMKgwqAgKGlmIChsaXN0 PyBoZWFkKQo+ICvCoMKgwqDCoMKgwqDCoCAoZm9sZC1yaWdodCBmbGF0dGVuMSBvdXQgaGVhZCkK PiArwqDCoMKgwqDCoMKgwqAgKGNvbnMgaGVhZCBvdXQpKSkKPiArwqAgKGZvbGQtcmlnaHQgZmxh dHRlbjEgJygpIGxzdCkpCj4gKwo+ICs7OyBUaGlzIGZ1bmN0aW9uIHRha2VzIGluIGEgcmVjb3Jk LCBvciBsaXN0LCBvciBhbnl0aGluZywgYW5kCj4gcmV0dXJucwo+ICs7OyBhIGxpc3Qgb2YgPG9w ZW5zbXRwZC10YWJsZT5zIGFzc3VtaW5nIHRoZSB0aGluZyB5b3UgcGFzc2VkIGludG8KPiBpdCBo YWQKPiArOzsgYW55IDxvcGVuc210cGQtdGFibGU+cy4KPiArOzsKPiArOzsgaXMgb2JqZWN0IHJl Y29yZD8gY2FsbCBmdW5jIG9uIGl0J3MgZmllbGRuYW1lcwo+ICs7OyBpcyBvYmplY3QgbGlzdD8g bG9vcCB0aHJvdWdoIGl0J3MgZmllbGRuYW1lcyBjYWxsaW5nIGZ1bmMgb24gaXQncwo+IHJlY29y ZHMKPiArOzsgaXMgb2JqZWN0ICNmIG9yIHN0cmluZz8gb3IgJygpPyAtPiAjZgo+ICsoZGVmaW5l IChnZXQtb3BlbnNtdHBkLXRhYmxlcyB2YWx1ZSkKPiArwqAgKGRlbGV0ZS1kdXBsaWNhdGVzCj4g K8KgwqAgKHJlbW92ZSBib29sZWFuPyAoZmxhdHRlbiA7OyB0dXJuIChsaXN0ICcoMSkgJygyICco MykpKSAtPiAnKDEgMgo+IDMpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKGNvbmQgKChvcGVuc210cGQtdGFibGU/IHZhbHVlKQo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgdmFsdWUpCj4gK8KgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKChyZWNvcmQ/IHZh bHVlKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKGxldCogKChyZWNvcmQtdHlwZSAocmVjb3JkLXR5cGUtCj4gZGVzY3JpcHRvciB2YWx1 ZSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgIChsaXN0LW9mLXJlY29yZC1maWVsZG5hbWVzCj4gKHJlY29yZC10 eXBlLWZpZWxkcyByZWNvcmQtdHlwZSkpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChtYXAgKGxhbWJkYSAoZmllbGRuYW1lKQo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgKGdldC1vcGVuc210cGQtdGFibGVzICgocmVjb3JkLQo+IGFjY2Vz c29yIHJlY29yZC10eXBlIGZpZWxkbmFtZSkgdmFsdWUpKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgbGlzdC1v Zi1yZWNvcmQtZmllbGRuYW1lcykpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgICgoYW5kIChsaXN0PyB2YWx1ZSkgKG5vdCAobnVsbD8gdmFs dWUpKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgIChtYXAgZ2V0LW9wZW5zbXRwZC10YWJsZXMgdmFsdWUpKQo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChlbHNlICNmKSkpKSkpCj4g Kwo+ICsoZGVmaW5lIChvcGVuc210cGQtY29uZmlndXJhdGlvbi1maWVsZG5hbWUtPnN0cmluZyBy ZWNvcmQgZmllbGRuYW1lLQo+IGFjY2Vzc29yIHJlY29yZC0+c3RyaW5nKQo+ICvCoCAoaWYgKGZp ZWxkbmFtZS1hY2Nlc3NvciByZWNvcmQpCj4gK8KgwqDCoMKgwqAgKGJlZ2luCj4gK8KgwqDCoMKg wqDCoMKgIChzdHJpbmctYXBwZW5kCj4gK8KgwqDCoMKgwqDCoMKgwqAgKGxpc3Qtb2YtcmVjb3Jk cy0+c3RyaW5nIChmaWVsZG5hbWUtYWNjZXNzb3IgcmVjb3JkKQo+IHJlY29yZC0+c3RyaW5nKSAi XG4iKSkKPiArwqDCoMKgwqDCoCAiIikpCj4gKwo+ICsoZGVmaW5lIChsaXN0LW9mLXJlY29yZHMt PnN0cmluZyBsaXN0LW9mLXJlY29yZHMgcmVjb3JkLT5zdHJpbmcpCj4gK8KgIChzdHJpbmctYXBw ZW5kCj4gK8KgwqAgKGNvbmQgKChub3QgKGxpc3Q/IGxpc3Qtb2YtcmVjb3JkcykpCj4gK8KgwqDC oMKgwqDCoMKgwqDCoCAocmVjb3JkLT5zdHJpbmcgbGlzdC1vZi1yZWNvcmRzKSkKPiArwqDCoMKg wqDCoMKgwqDCoCAoZWxzZQo+ICvCoMKgwqDCoMKgwqDCoMKgwqAgKGxldCBsb29wIChbbGlzdCBs aXN0LW9mLXJlY29yZHNdKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChpZiAobnVsbD8gbGlz dCkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICIiCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoCAoc3RyaW5nLWFwcGVuZAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAocmVjb3JkLT5zdHJpbmcgKGNhciBsaXN0KSkKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgKGxvb3AgKGNkciBsaXN0KSkpKSkpKSkpCj4gKwo+ICsoZGVmaW5l IChvcGVuc210cGQtY29uZmlndXJhdGlvbi0+c3RyaW5nIHJlY29yZCkKPiArwqAgKHN0cmluZy1h cHBlbmQKPiArwqDCoCAob3BlbnNtdHBkLWNvbmZpZ3VyYXRpb24tZmllbGRuYW1lLT5zdHJpbmcg cmVjb3JkIG9wZW5zbXRwZC0KPiBjb25maWd1cmF0aW9uLWJvdW5jZQo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxhbWJkYSAoJWJvdW5jZSkKPiArwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoaWYgJWJvdW5jZQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3Qtb2YtCj4gc3RyaW5ncy0+c3RyaW5nICVi b3VuY2UpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAi IikpKQo+ICvCoMKgIChvcGVuc210cGQtY29uZmlndXJhdGlvbi1maWVsZG5hbWUtPnN0cmluZyBy ZWNvcmQgb3BlbnNtdHBkLQo+IGNvbmZpZ3VyYXRpb24tc210cAo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgb3BlbnNtdHBkLXNtdHAtCj4gPnN0cmluZykKPiArwqDCoCAob3Bl bnNtdHBkLWNvbmZpZ3VyYXRpb24tZmllbGRuYW1lLT5zdHJpbmcgcmVjb3JkIG9wZW5zbXRwZC0K PiBjb25maWd1cmF0aW9uLXNycwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAg b3BlbnNtdHBkLQo+IGNvbmZpZ3VyYXRpb24tc3JzLT5zdHJpbmcpCj4gK8KgwqAgKG9wZW5zbXRw ZC1jb25maWd1cmF0aW9uLWZpZWxkbmFtZS0+c3RyaW5nIHJlY29yZCBvcGVuc210cGQtCj4gY29u ZmlndXJhdGlvbi1xdWV1ZQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgb3Bl bnNtdHBkLQo+IGNvbmZpZ3VyYXRpb24tcXVldWUtPnN0cmluZykKPiArwqDCoCA7OyB3cml0ZSBv dXQgdGhlIG10YS1tYXgtZGVmZXJyZWQKPiArwqDCoCAob3BlbnNtdHBkLWNvbmZpZ3VyYXRpb24t ZmllbGRuYW1lLT5zdHJpbmcgcmVjb3JkIG9wZW5zbXRwZC0KPiBjb25maWd1cmF0aW9uLW10YS1t YXgtZGVmZXJyZWQKPiArwqDCoMKgIChsYW1iZGEgKHZhcikKPiArwqDCoMKgwqDCoCAoc3RyaW5n LWFwcGVuZCAibXRhIG1heC1kZWZlcnJlZCAiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgKG51bWJlci0+c3RyaW5nIChvcGVuc210cGQtY29uZmlndXJhdGlvbi1t dGEtCj4gbWF4LWRlZmVycmVkIHJlY29yZCkpICJcbiIpKSkKPiArwqDCoCA7O3dyaXRlIG91dCBh bGwgdGhlIHRhYmxlcwo+ICvCoMKgIChvcGVuc210cGQtY29uZmlndXJhdGlvbi1maWVsZG5hbWUt PnN0cmluZyByZWNvcmQgZ2V0LW9wZW5zbXRwZC0KPiB0YWJsZXMgb3BlbnNtdHBkLXRhYmxlLT5z dHJpbmcpCj4gK8KgwqAgOzsgd3JpdGUgb3V0IGFsbCB0aGUgY2FzCj4gK8KgwqAgKG9wZW5zbXRw ZC1jb25maWd1cmF0aW9uLWZpZWxkbmFtZS0+c3RyaW5nIHJlY29yZCBvcGVuc210cGQtCj4gY29u ZmlndXJhdGlvbi1jYXMgb3BlbnNtdHBkLWNhLT5zdHJpbmcpCj4gK8KgwqAgOzsgd3JpdGUgb3V0 IGFsbCB0aGUgcGtpcwo+ICvCoMKgIChvcGVuc210cGQtY29uZmlndXJhdGlvbi1maWVsZG5hbWUt PnN0cmluZyByZWNvcmQgZ2V0LW9wZW5zbXRwZC0KPiBwa2lzIG9wZW5zbXRwZC1wa2ktPnN0cmlu ZykKPiArwqDCoCA7OyB3cml0ZSBhbGwgb2YgdGhlIGxpc3Rlbi1vbi1yZWNvcmRzCj4gK8KgwqAg KG9wZW5zbXRwZC1jb25maWd1cmF0aW9uLWZpZWxkbmFtZS0+c3RyaW5nIHJlY29yZCBvcGVuc210 cGQtCj4gY29uZmlndXJhdGlvbi1saXN0ZW4tb25zCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCBvcGVuc210cGQtaW50ZXJmYWNlLQo+ID5zdHJpbmcpCj4gK8KgwqAgKG9wZW5z bXRwZC1jb25maWd1cmF0aW9uLWZpZWxkbmFtZS0+c3RyaW5nIHJlY29yZCBvcGVuc210cGQtCj4g Y29uZmlndXJhdGlvbi1saXN0ZW4tb24tc29ja2V0Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCBvcGVuc210cGQtc29ja2V0LQo+ID5zdHJpbmcpCj4gK8KgwqAgOzsgd3JpdGUg YWxsIHRoZSBhY3Rpb25zCj4gK8KgwqAgKG9wZW5zbXRwZC1jb25maWd1cmF0aW9uLWZpZWxkbmFt ZS0+c3RyaW5nIHJlY29yZCBnZXQtb3BlbnNtdHBkLQo+IGFjdGlvbnMKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgIG9wZW5zbXRwZC1hY3Rpb24tCj4gPnN0cmluZykKPiArwqDC oCA7OyB3cml0ZSBhbGwgb2YgdGhlIG1hdGNoZXMKPiArwqDCoCAob3BlbnNtdHBkLWNvbmZpZ3Vy YXRpb24tZmllbGRuYW1lLT5zdHJpbmcgcmVjb3JkIG9wZW5zbXRwZC0KPiBjb25maWd1cmF0aW9u LW1hdGNoZXMgb3BlbnNtdHBkLW1hdGNoLT5zdHJpbmcpKSkKPiArCj4gKzs7IEZJWE1FL1RPRE8g c2hvdWxkIEkgdXNlIGZvcm1hdCBoZXJlIHNyZmktMjggPwo+ICs7OyB3ZWIuc2NtIG5naW54IGRv ZXMgYSAoZm9ybWF0ICNmICJzdHJpbmciICJhbm90aGVyIHN0cmluZyIpCj4gKzs7IHRoaXMgY291 bGQgYmUgYSBsaXN0IGxpa2UgKGxpc3QgKGZpbGUtYXBwZW5kIG9wZW5zbXRwZC1ka2ltc2lnbgo+ ICIvbGliZXhlYy9maWx0ZXIiKSAiLWQgZ251Y29kZS5tZSAtcyAvcGF0aC90by9zZWxlY3Rvci5j ZXJ0IikKPiArOzsgVGhlbiBvcGVuc210cGQtY29uZmlndXJhdGlvbi0+bWl4ZWQtdGV4dC1maWxl IGNvdWxkIGJlIHJld3JpdHRlbgo+IHRvIGJlIHNvbWV0aGluZyBsaWtlCj4gKzs7IChtaXhlZC10 ZXh0LWZpbGUgKGV2YWwgYChzdHJpbmctYXBwZW5kIChvcGVuc210cGQtY29uZmlndXJhdGlvbi0K PiBmaWVsZG5hbWUtPnN0cmluZyAuLi4pKSAoZ251IHNlcnZpY2VzIG1haWwpKSkKPiArKGRlZmlu ZSAob3BlbnNtdHBkLWNvbmZpZ3VyYXRpb24tPm1peGVkLXRleHQtZmlsZSByZWNvcmQpCj4gK8Kg IDs7IHNob3VsZCBJIHVzZSB0aGlzIG5hbWVkIGxldCwgb3Igc2hvdWxkIEkgZ2l2ZSB0aGlzIGEg bmFtZSwgb3IKPiBub3QgdXNlIGl0IGF0IGFsbC4uLgo+ICvCoCA7OyBlZzogKHdyaXRlLWFsbC1m aWVsZG5hbWVzIChsaXN0IChjb25zIGZpZWxkbmFtZSBmaWVsZG5hbWUtCj4gPnN0cmluZykgKGNv bnMgZmllbGRuYW1lMiBmaWVsZG5hbWUtPnN0cmluZykpKQo+ICvCoCA7OyAobGV0IGxvb3AgKFts aXN0IChsaXN0IChjb25zIG9wZW5zbXRwZC1jb25maWd1cmF0aW9uLWluY2x1ZGVzCj4gKGxhbWJk YSAoc3RyaW5nKQo+ICvCoCA7O8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgCj4gKHN0cmluZy1hcHBlbmQKPiArwqAK PiA7O8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqAKPiAiaW5jbHVkZSBcIiIgc3RyaW5nICJcIlxuIikpKQo+ICvC oCA7O8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGNvbnMg b3BlbnNtdHBkLWNvbmZpZ3VyYXRpb24tc210cAo+IG9wZW5zbXRwZC1zbXRwLT5zdHJpbmcpCj4g K8KgIDs7wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoY29u cyBvcGVuc210cGQtY29uZmlndXJhdGlvbi1zcnMKPiBvcGVuc210cGQtc3JzLT5zdHJpbmcpKV0p Cj4gK8KgIDs7wqDCoCAoaWYgKG51bGw/IGxpc3QpCj4gK8KgIDs7wqDCoMKgwqDCoMKgICIiCj4g K8KgIDs7wqDCoMKgwqDCoMKgIChzdHJpbmctYXBwZW5kIChvcGVuc210cGQtY29uZmlndXJhdGlv bi1maWVsZG5hbWUtPnN0cmluZwo+IHJlY29yZAo+ICvCoCA7O8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgCj4gKGNh YXIgbGlzdCkKPiArwqAgOzvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoAo+IChjZGFyIGxpc3QpKQo+ICvCoCA7O8Kg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobG9vcCAoY2RyIGxpc3Qp KSkpKQo+ICsKPiArwqAgKGFwcGx5IG1peGVkLXRleHQtZmlsZSAic210cGQuY29uZiIKPiArwqDC oMKgwqDCoMKgwqDCoCA7OyB3cml0ZSBvdXQgdGhlIGluY2x1ZGVzCj4gK8KgwqDCoMKgwqDCoMKg wqAgKGZsYXR0ZW4gKGxpc3QKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg IChvcGVuc210cGQtY29uZmlndXJhdGlvbi1maWVsZG5hbWUtPnN0cmluZyByZWNvcmQKPiBvcGVu c210cGQtY29uZmlndXJhdGlvbi1pbmNsdWRlcwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoAo+IG9wZW5zbXRwZC1jb25m aWd1cmF0aW9uLWxpc3Rlbi0+c3RyaW5nKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqAgOzsgVE9ETyBzaG91bGQgSSBjaGFuZ2UgdGhlIGJlbG93IGxpbmUgb2YgY29kZQo+ IGludG8gdGhlc2UgdHdvIGxpbmVzIG9mIGNvZGU/Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoCA7OyhvcGVuc210cGQtY29uZmlndXJhdGlvbi1maWVsZG5hbWUtPnN0cmlu Zwo+IHJlY29yZCBnZXQtb3BlbnNtdHBkLWZpbHRlcnMtYW5kLWZpbHRlci1waGFzZXMgb3BlbnNt dHBkLWZpbHRlci1hbmQtCj4gZmlsdGVyLXBoYXNlLT5zdHJpbmcpCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCA7OyhvcGVuc210cGQtY29uZmlndXJhdGlvbi1maWVsZG5h bWUtPnN0cmluZwo+IHJlY29yZCBnZXQtb3BlbnNtdHBkLWZpbHRlci1jaGFpbnMgb3BlbnNtdHBk LWZpbHRlci1jaGFpbi0+c3RyaW5nKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgOzsgd3JpdGUgb3V0IGFsbCB0aGUgZmlsdGVycwo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1maWx0ZXJzLT5saXN0LW9mLXN0cmluZ3MtYW5k LWdleHBzCj4gKGdldC1vcGVuc210cGQtZmlsdGVycyByZWNvcmQpKQo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1jb25maWd1cmF0aW9uLT5zdHJpbmcg cmVjb3JkKSkpKSkKPiArCj4gKwo+IMKgKGRlZmluZSAlZGVmYXVsdC1vcGVuc210cGQtY29uZmln LWZpbGUKPiDCoMKgIChwbGFpbi1maWxlICJzbXRwZC5jb25mIiAiCj4gwqBsaXN0ZW4gb24gbG8K PiBAQCAtMTY2OCw3ICszNzI0LDcgQEAgKGRlZmluZSAlZGVmYXVsdC1vcGVuc210cGQtY29uZmln LWZpbGUKPiDCoG1hdGNoIGZyb20gbG9jYWwgZm9yIGFueSBhY3Rpb24gb3V0Ym91bmQKPiDCoCIp KQo+IMKgCj4gLShkZWZpbmUgb3BlbnNtdHBkLXNoZXBoZXJkLXNlcnZpY2UKPiArKGRlZmluZSAo b3BlbnNtdHBkLXNoZXBoZXJkLXNlcnZpY2UgY29uZmlnKQo+IMKgwqAgKG1hdGNoLWxhbWJkYQo+ IMKgwqDCoMKgICgoJCA8b3BlbnNtdHBkLWNvbmZpZ3VyYXRpb24+IHBhY2thZ2UgY29uZmlnLWZp bGUpCj4gwqDCoMKgwqDCoCAobGlzdCAoc2hlcGhlcmQtc2VydmljZQo+IEBAIC0xNjc3LDcgKzM3 MzMsOCBAQCAoZGVmaW5lIG9wZW5zbXRwZC1zaGVwaGVyZC1zZXJ2aWNlCj4gwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIChkb2N1bWVudGF0aW9uICJSdW4gdGhlIE9wZW5TTVRQRCBkYWVtb24uIikK PiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHN0YXJ0IChsZXQgKChzbXRwZCAoZmlsZS1hcHBl bmQgcGFja2FnZQo+ICIvc2Jpbi9zbXRwZCIpKSkKPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgI34obWFrZS1mb3JrZXhlYy1jb25zdHJ1Y3Rvcgo+IC3CoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChsaXN0ICMkc210cGQgIi1m IiAjJGNvbmZpZy1maWxlKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgIChsaXN0ICMkc210cGQgIi1mIiAob3IgIyRjb25maWctZmlsZQo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAjJChvcGVuc210cGQtCj4gY29uZmlndXJhdGlv bi0+bWl4ZWQtdGV4dC1maWxlIGNvbmZpZykpKQo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAjOnBpZC1maWxlICIvdmFyL3J1bi9zbXRwZC5waWQiKSkp Cj4gwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzdG9wICN+KG1ha2Uta2lsbC1kZXN0cnVjdG9y KSkpKSkpKQo+IMKgCj4gQEAgLTE3MDAsMTAgKzM3NTcsMTEgQEAgKGRlZmluZSAlb3BlbnNtdHBk LWFjY291bnRzCj4gwqDCoMKgwqDCoMKgwqDCoMKgIChob21lLWRpcmVjdG9yeSAiL3Zhci9lbXB0 eSIpCj4gwqDCoMKgwqDCoMKgwqDCoMKgIChzaGVsbCAoZmlsZS1hcHBlbmQgc2hhZG93ICIvc2Jp bi9ub2xvZ2luIikpKSkpCj4gwqAKPiAtKGRlZmluZSBvcGVuc210cGQtYWN0aXZhdGlvbgo+ICso ZGVmaW5lIChvcGVuc210cGQtYWN0aXZhdGlvbiBjb25maWcpCj4gwqDCoCAobWF0Y2gtbGFtYmRh Cj4gwqDCoMKgwqAgKCgkIDxvcGVuc210cGQtY29uZmlndXJhdGlvbj4gcGFja2FnZSBjb25maWct ZmlsZSkKPiAtwqDCoMKgwqAgKGxldCAoKHNtdHBkIChmaWxlLWFwcGVuZCBwYWNrYWdlICIvc2Jp bi9zbXRwZCIpKSkKPiArwqDCoMKgwqAgKGxldCAoKHNtdHBkIChmaWxlLWFwcGVuZCBwYWNrYWdl ICIvc2Jpbi9zbXRwZCIpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoCAoY29uZmlndXJhdGlvbiAo b3BlbnNtdHBkLWNvbmZpZ3VyYXRpb24tPm1peGVkLXRleHQtZmlsZQo+IGNvbmZpZykpKQo+IMKg wqDCoMKgwqDCoMKgICN+KGJlZ2luCj4gwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAodXNlLW1vZHVs ZXMgKGd1aXggYnVpbGQgdXRpbHMpKQo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgOzsgQ3JlYXRl IG1ib3ggYW5kIHNwb29sIGRpcmVjdG9yaWVzLgo+IEBAIC0xNzExLDcgKzM3NjksMTIgQEAgKGRl ZmluZSBvcGVuc210cGQtYWN0aXZhdGlvbgo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG1rZGly LXAgIi92YXIvc3Bvb2wvc210cGQiKQo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGNobW9kICIv dmFyL3Nwb29sL3NtdHBkIiAjbzcxMSkKPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChta2Rpci1w ICIvdmFyL3Nwb29sL21haWwiKQo+IC3CoMKgwqDCoMKgwqDCoMKgwqDCoCAoY2htb2QgIi92YXIv c3Bvb2wvbWFpbCIgI283MTEpKSkpKSkKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGNobW9kICIv dmFyL3Nwb29sL21haWwiICNvNzExKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZGlzcGxheSAo c3RyaW5nLWFwcGVuZCAiY2hlY2tpbmcgc3ludGF4IG9mICIKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9yCj4g K8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgIyRjb25maWctZmlsZQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICMkY29uZmlndXJh dGlvbikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqAgIlxuIikpKSkpKSkKPiDCoAo+IMKgKGRlZmluZSAlb3BlbnNt dHBkLXBhbS1zZXJ2aWNlcwo+IMKgwqAgKGxpc3QgKHVuaXgtcGFtLXNlcnZpY2UgInNtdHBkIikp KQo+IGRpZmYgLS1naXQgYS9nbnUvdGVzdHMvbWFpbC5zY20gYi9nbnUvdGVzdHMvbWFpbC5zY20K PiBpbmRleCBmMTM3NTFiNzJmLi4xYmFjOWY1MGEyIDEwMDY0NAo+IC0tLSBhL2dudS90ZXN0cy9t YWlsLnNjbQo+ICsrKyBiL2dudS90ZXN0cy9tYWlsLnNjbQo+IEBAIC0zNyw2ICszNyw3IEBAIChk ZWZpbmUtbW9kdWxlIChnbnUgdGVzdHMgbWFpbCkKPiDCoMKgICM6dXNlLW1vZHVsZSAoZ3VpeCBn ZXhwKQo+IMKgwqAgIzp1c2UtbW9kdWxlIChndWl4IHN0b3JlKQo+IMKgwqAgIzp1c2UtbW9kdWxl IChpY2UtOSBmdHcpCj4gK8KgICM6dXNlLW1vZHVsZSAoc3JmaSBzcmZpLTY0KQo+IMKgwqAgIzpl eHBvcnQgKCV0ZXN0LW9wZW5zbXRwZAo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAldGVzdC1l eGltCj4gwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICV0ZXN0LWRvdmVjb3QKPiBAQCAtMTY1LDYg KzE2NiwzNjAgQEAgKGRlZmluZSAldGVzdC1vcGVuc210cGQKPiDCoMKgwqAgKGRlc2NyaXB0aW9u ICJTZW5kIGFuIGVtYWlsIHRvIGEgcnVubmluZyBPcGVuU01UUEQgc2VydmVyLiIpCj4gwqDCoMKg ICh2YWx1ZSAocnVuLW9wZW5zbXRwZC10ZXN0KSkpKQo+IMKgCj4gKzs7IHRyeWluZyB0byBjcmVh dGUgYSBiYWQgcmVjb3JkLCBzaG91bGQgcmVzdWx0IGluIGFuIGVycm9yLgo+ICs7OyB0aGlzIGZ1 bmN0aW9uIHNob3VsZCBiZSBhYmxlIHJldHVybiwgaW5zdGVhZCBpdCBzaG91bGQgdGhyb3cgYW4K PiBlcnJvcgo+ICsoZGVmaW5lIChjcmVhdGUtYmFkLXJlY29yZCByZWNvcmQpCj4gK8KgIDs7IFRP RE8gd2h5IGlzIHRoaXMgbm90IHdvcmtpbmcKPiArwqAgKHdpdGgtb3V0cHV0LXRvLXBvcnQgKCVt YWtlLXZvaWQtcG9ydCAidyIpCj4gK8KgwqDCoCAobGFtYmRhICgpICh3aGVuIHJlY29yZCAjZikp KSkKPiArCj4gKzs7IGlmIHRoaXMgY2FsbGVyIGZ1bmN0aW9uIGlzIHJlYWNoZWQsIHRoZW4gdHJ5 aW5nIHRvIGNyZWF0ZSB0aGUgYmFkCj4gcmVjb3JkCj4gKzs7IHJlc3VsdGVkIGluIGFuIGVycm9y LsKgIFNvIHJldHVybiB0cnVlLgo+ICsoZGVmaW5lIChyZXR1cm4tdHJ1ZSBlcnJvciBhcmcpCj4g K8KgICN0KQo+ICsKPiArOzsgdHdvIGZpbHRlcnMgd2l0aCB0aGUgc2FtZSBuYW1lCj4gKyhkZWZp bmUgKGJhZC1pbnRlcmZhY2UxKQo+ICvCoCAoY3JlYXRlLWJhZC1yZWNvcmQKPiArwqDCoCAob3Bl bnNtdHBkLWludGVyZmFjZQo+ICvCoMKgwqAgKGludGVyZmFjZSAibG8iKQo+ICvCoMKgwqAgKGZp bHRlcnMgKGxpc3QKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1maWx0 ZXIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobmFtZSAiZGtpbXNpZ24iKQo+ICvC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChleGVjICN0KQo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIChwcm9jIChsaXN0IChmaWxlLWFwcGVuZCBvcGVuc210cGQtZmlsdGVyLWRr aW1zaWduCj4gIi9saWJleGVjL29wZW5zbXRwZC9maWx0ZXItZGtpbXNpZ24iKQo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICIgLWQgZ251Y29k ZS5tZSAtcyAyMDIxLTA5LTIyIC1jCj4gcmVsYXhlZC9yZWxheGVkIC1rICIKPiArwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAicmFuZG8gc3RyaW5n Igo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg ICIvZXRjL2RraW0vcHJpdmF0ZS5rZXkgIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJ1c2VyIG5vYm9keSBncm91cCBub2dyb3VwIikpKQo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3BlbnNtdHBkLWZpbHRlcgo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgIChuYW1lICJka2ltc2lnbiIpCj4gK8KgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgKGV4ZWMgI3QpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAg KHByb2MgKGxpc3QgKGZpbGUtYXBwZW5kIG9wZW5zbXRwZC1maWx0ZXItZGtpbXNpZ24KPiAiL2xp YmV4ZWMvb3BlbnNtdHBkL2ZpbHRlci1ka2ltc2lnbiIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIiAtZCBnbnVjb2RlLm1lIC1zIDIwMjEt MDktMjIgLWMKPiByZWxheGVkL3JlbGF4ZWQgLWsgIgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICIvZXRjL2RraW0vcHJpdmF0ZS5rZXkgIgo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJ1 c2VyIG5vYm9keSBncm91cCBub2dyb3VwIikpKSkpKSkpCj4gKwo+ICs7OyBkdXBsaWNhdGUgZmls dGVyIG5hbWVzCj4gKyhkZWZpbmUgKGJhZC1pbnRlcmZhY2UyKQo+ICvCoCAoY3JlYXRlLWJhZC1y ZWNvcmQKPiArwqDCoCAob3BlbnNtdHBkLWludGVyZmFjZQo+ICvCoMKgwqAgKGZpbHRlcnMgKGxp c3QKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1maWx0ZXItcGhhc2UK PiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobmFtZSAic3JjIikKPiArwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoCAocGhhc2UgImNvbm5lY3QiKQo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgIChvcHRpb25zCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAo bGlzdAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3BlbnNtdHBkLW9wdGlv bgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcHRpb24gImZjcmRucyIp Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG5vdCAjdCkpKSkKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZGVjaXNpb24gImp1bmsiKSkKPiArwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1maWx0ZXItcGhhc2UKPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoCAobmFtZSAic3JjIikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoCAocGhhc2UgImhlbG8iKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcHRp b25zCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdAo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3BlbnNtdHBkLW9wdGlvbgo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcHRpb24gInJkbnMiKQo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgIChub3QgI3QpKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKGRlY2lzaW9uICJqdW5rIikpKSkpKSkKPiArCj4gKyA7OyBpbXByb3BlciBwaGFzZSBu YW1lCj4gKyhkZWZpbmUgKGJhZC1maWx0ZXItcGhhc2UxKQo+ICvCoMKgwqAgKGNyZWF0ZS1iYWQt cmVjb3JkCj4gK8KgwqDCoMKgIChvcGVuc210cGQtZmlsdGVyLXBoYXNlCj4gK8KgwqDCoMKgwqAg KG5hbWUgImZpbHRlciIpCj4gK8KgwqDCoMKgwqAgKHBoYXNlICJ3cm9uZ1N0cmluZyIpCj4gK8Kg wqDCoMKgwqAgKGRlY2lzaW9uICJieXBhc3MiKQo+ICvCoMKgwqDCoMKgIChvcHRpb25zCj4gK8Kg wqDCoMKgwqDCoCAobGlzdAo+ICvCoMKgwqDCoMKgwqDCoCAob3BlbnNtdHBkLW9wdGlvbgo+ICvC oMKgwqDCoMKgwqDCoMKgIChvcHRpb24gImF1dGgiKSkpKSkpKQo+ICsKPiArOzsgZGVjaXNpb24g cmVqZWN0IHJlcXVpcmVzIHlvdSB0byBoYXZlIGEKPiArOzsgY29ycmVzcG9uZGluZyBmaWVsZG5h bWUgJ21lc3NhZ2UnIHdpdGggdmFsdWUgb2Ygc3RyaW5nLgo+ICsoZGVmaW5lIChiYWQtZmlsdGVy LXBoYXNlMikKPiArwqAgKGNyZWF0ZS1iYWQtcmVjb3JkCj4gK8KgwqAgKG9wZW5zbXRwZC1pbnRl cmZhY2UKPiArwqDCoMKgIChmaWx0ZXJzIChsaXN0Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIChvcGVuc210cGQtZmlsdGVyLXBoYXNlCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqAgKG5hbWUgInNyYyIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHBoYXNlICJj b25uZWN0IikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3B0aW9ucwo+ICvCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGxpc3QKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1vcHRpb24KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAob3B0aW9uICJzcmMiKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgIChkYXRhIChvcGVuc210cGQtdGFibGUKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChuYW1lICJzcmMtdGFibGUiKQo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGRhdGEgKGxpc3QgImNhdCIg ImhhdCIpKSkpKSkpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGRlY2lzaW9uICJy ZWplY3QiKSkpKSkpKQo+ICsKPiArOzsgbWVzc2FnZSBuZWVkcyB0byBzdGFydCB3aXRoIDR4eCBv ciA1eHgKPiArKGRlZmluZSAoYmFkLWZpbHRlci1waGFzZTMpCj4gK8KgIChjcmVhdGUtYmFkLXJl Y29yZAo+ICvCoMKgIChvcGVuc210cGQtaW50ZXJmYWNlCj4gK8KgwqDCoCAoZmlsdGVycyAobGlz dAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3BlbnNtdHBkLWZpbHRlci1waGFzZQo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChuYW1lICJzcmMiKQo+ICvCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgIChwaGFzZSAiY29ubmVjdCIpCj4gK8KgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqAgKG9wdGlvbnMKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChs aXN0Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcGVuc210cGQtb3B0aW9u Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wdGlvbiAic3JjIikKPiAr wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZGF0YSAob3BlbnNtdHBkLXRhYmxl Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobmFt ZSAic3JjLXRhYmxlIikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgIChkYXRhIChsaXN0ICJjYXQiICJoYXQiKSkpKSkpKQo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgIChkZWNpc2lvbiAicmVqZWN0IikKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAobWVzc2FnZSAiMzIyIEJhZCBkYXRhISIpKSkpKSkpCj4gKwo+ICsgOzsgdGhl cmUgbmVlZHMgdG8gYmUgYSB2YWx1ZSBoZXJlLsKgIHJld3JpdGUgcmVxdWlyZXMgYSB2YWx1ZSEK PiArKGRlZmluZSAoYmFkLWZpbHRlci1waGFzZTQpCj4gK8KgIChjcmVhdGUtYmFkLXJlY29yZAo+ ICvCoMKgIChvcGVuc210cGQtaW50ZXJmYWNlCj4gK8KgwqDCoCAoZmlsdGVycwo+ICvCoMKgwqDC oCAobGlzdAo+ICvCoMKgwqDCoMKgIChvcGVuc210cGQtZmlsdGVyLXBoYXNlCj4gK8KgwqDCoMKg wqDCoCAobmFtZSAibm9GUkROUyIpCj4gK8KgwqDCoMKgwqDCoCAocGhhc2UgImNvbW1pdCIpCj4g K8KgwqDCoMKgwqDCoCAob3B0aW9ucyAobGlzdCAob3BlbnNtdHBkLW9wdGlvbgo+ICvCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3B0aW9uICJmY3JkbnMiKQo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobm90ICN0KSkp KQo+ICvCoMKgwqDCoMKgwqAgKGRlY2lzaW9uICJyZXdyaXRlIikpCj4gK8KgwqDCoMKgwqAgKSkp KSkKPiArCj4gKzs7IGZpZWxkbmFtZSAnZGVjaXNpb24nIHdpdGggdmFsdWUgImp1bmsiIG9yICJi eXBhc3MiLCB0aGVuCj4gZmllbGRuYW1lICdtZXNzYWdlJyBhbmQgJ3ZhbHVlJwo+ICs7OyBtdXN0 IE5PVCBiZSBkZWZpbmVkCj4gKyhkZWZpbmUgKGJhZC1maWx0ZXItcGhhc2U1KQo+ICvCoCAoY3Jl YXRlLWJhZC1yZWNvcmQKPiArwqDCoCAob3BlbnNtdHBkLWludGVyZmFjZQo+ICvCoMKgwqAgKGZp bHRlcnMKPiArwqDCoMKgwqAgKGxpc3QKPiArwqDCoMKgwqDCoCAob3BlbnNtdHBkLWZpbHRlci1w aGFzZQo+ICvCoMKgwqDCoMKgwqAgKG5hbWUgIm5vRlJETlMiKQo+ICvCoMKgwqDCoMKgwqAgKHBo YXNlICJjb21taXQiKQo+ICvCoMKgwqDCoMKgwqAgKG9wdGlvbnMgKGxpc3QgKG9wZW5zbXRwZC1v cHRpb24KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9w dGlvbiAiZmNyZG5zIikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKSkpCj4gK8KgwqDCoMKgwqDCoCAoZGVjaXNpb24gImp1bmsiKQo+ICvCoMKgwqDCoMKg wqAgKG1lc3NhZ2UgIlRoaXMgaXMgbm90IGEgZ29vZCBlbWFpbC4iKSkpKSkpKQo+ICsKPiArOzsg eW91IGNhbm5vdCBqdW5rIG9uIHBoYXNlIGNvbW1pdC7CoCBZb3UgbmVlZCB0byB1c2UgYW4gZWFy aWxlcgo+IHBoYXNlLgo+ICsoZGVmaW5lIChiYWQtZmlsdGVyLXBoYXNlNikKPiArwqAgKGNyZWF0 ZS1iYWQtcmVjb3JkCj4gK8KgwqAgKG9wZW5zbXRwZC1pbnRlcmZhY2UKPiArwqDCoMKgIChmaWx0 ZXJzCj4gK8KgwqDCoMKgIChsaXN0Cj4gK8KgwqDCoMKgwqAgKG9wZW5zbXRwZC1maWx0ZXItcGhh c2UKPiArwqDCoMKgwqDCoMKgIChuYW1lICJqdW5rLWFmdGVyLWNvbW1pdCIpCj4gK8KgwqDCoMKg wqDCoCAob3B0aW9ucyAobGlzdCAob3BlbnNtdHBkLW9wdGlvbgo+ICvCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3B0aW9uICJmY3JkbnMiKSkpKQo+ICvCoMKg wqDCoMKgwqAgKHBoYXNlICJjb21taXQiKQo+ICvCoMKgwqDCoMKgwqAgKGRlY2lzaW9uICJqdW5r IikpKSkpKSkKPiArCj4gKzs7IFRPRE8gZml4IHRoaXMgdGVzdAo+ICs7OyB0d28gZmNyZG5zIG9w dGlvbnMgcmVjb3Jkcwo+ICsoZGVmaW5lIChiYWQtZmlsdGVyLXBoYXNlNykKPiArwqAgKGNyZWF0 ZS1iYWQtcmVjb3JkCj4gK8KgwqAgKG9wZW5zbXRwZC1maWx0ZXItcGhhc2UKPiArwqDCoMKgIChu YW1lICJpbnZhbGlkLWZjcmRucyIpCj4gK8KgwqDCoCAocGhhc2UgImNvbm5lY3QiKQo+ICvCoMKg wqAgKG9wdGlvbnMKPiArwqDCoMKgwqAgKGxpc3QgKG9wZW5zbXRwZC1vcHRpb24KPiArwqDCoMKg wqDCoMKgwqDCoMKgwqDCoCAob3B0aW9uICJmY3JkbnMiKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgIChub3QgI3QpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3BlbnNtdHBkLW9wdGlvbgo+ ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcHRpb24gImZjcmRucyIpCj4gK8KgwqDCoMKgwqDC oMKgwqDCoMKgwqAgKG5vdCAjZikpKSkKPiArwqDCoMKgIChkZWNpc2lvbiAicmVqZWN0IikKPiAr wqDCoMKgIChtZXNzYWdlICI0MjIgTm8gdmFsaWQgZmNyZG5zLiIpKSkpCj4gKwo+ICs7OyBvcHRp b24gc3JjIHJlcXVpcmVzIGEgdGFibGUKPiArOzsgVE9ETyBtYXliZSBjaGVjayBmb3Igb3RoZXIg b3B0aW9ucyByZXF1aXJpbmcgYSB0YWJsZQo+ICsoZGVmaW5lIChiYWQtZmlsdGVyLXBoYXNlOCkK PiArwqAgKGNyZWF0ZS1iYWQtcmVjb3JkCj4gK8KgwqAgKG9wZW5zbXRwZC1maWx0ZXItcGhhc2UK PiArwqDCoMKgIChuYW1lICJmaWx0ZXIiKQo+ICvCoMKgwqAgKHBoYXNlICJoZWxvIikKPiArwqDC oMKgIChkZWNpc2lvbiAiYnlwYXNzIikKPiArwqDCoMKgIChvcHRpb25zCj4gK8KgwqDCoMKgIChs aXN0Cj4gK8KgwqDCoMKgwqAgKG9wZW5zbXRwZC1vcHRpb24KPiArwqDCoMKgwqDCoMKgIChvcHRp b24gInNyYyIpKSkpKSkpCj4gKwo+ICs7OyBvcHRpb24gZmNyZG5zIGNhbm5vdCBoYXZlIGRhdGEg ZGVmaW5lZC4KPiArKGRlZmluZSAoYmFkLWZpbHRlci1waGFzZTkpCj4gK8KgIChjcmVhdGUtYmFk LXJlY29yZAo+ICvCoMKgIChvcGVuc210cGQtZmlsdGVyLXBoYXNlCj4gK8KgwqDCoCAobmFtZSAi ZmlsdGVyIikKPiArwqDCoMKgIChwaGFzZSAiaGVsbyIpCj4gK8KgwqDCoCAoZGVjaXNpb24gImJ5 cGFzcyIpCj4gK8KgwqDCoCAob3B0aW9ucwo+ICvCoMKgwqDCoCAobGlzdAo+ICvCoMKgwqDCoMKg IChvcGVuc210cGQtb3B0aW9uCj4gK8KgwqDCoMKgwqDCoCAob3B0aW9uICJmY3JkbnMiKQo+ICvC oMKgwqDCoMKgwqAgKGRhdGEgKG9wZW5zbXRwZC10YWJsZQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgIChuYW1lICJ0YWJsZSIpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAg KGRhdGEgKGxpc3QgImhlbGxvIiAiY2F0IikpKSkpKSkpKSkKPiArCj4gKwo+ICs7OyB0aGlzIHNo b3VsZCBiZSAobGlzdCAuLi4pIGluc3RlYWQgb2YgJyggLi4uKQo+ICsoZGVmaW5lIChiYWQtbWF0 Y2gxKQo+ICvCoCAoY3JlYXRlLWJhZC1yZWNvcmQKPiArwqDCoCAob3BlbnNtdHBkLW1hdGNoCj4g K8KgwqDCoCAob3B0aW9ucwo+ICvCoMKgwqDCoCAnKChvcGVuc210cGQtb3B0aW9uCj4gK8KgwqDC oMKgwqDCoMKgIChvcHRpb24gImZvciBhbnkiKSkpKQo+ICvCoMKgwqAgKGFjdGlvbgo+ICvCoMKg wqDCoCAob3BlbnNtdHBkLXJlbGF5KSkpKSkKPiArCj4gKwo+ICs7OyBkdXBsY2F0ZSAiZm9yIiBv cHRpb25zCj4gKyhkZWZpbmUgKGJhZC1tYXRjaDIpCj4gK8KgIChjcmVhdGUtYmFkLXJlY29yZAo+ ICvCoMKgIChvcGVuc210cGQtbWF0Y2gKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3B0aW9u cyAobGlzdAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9w ZW5zbXRwZC1vcHRpb24KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqAgKG9wdGlvbiAiZm9yIGFueSIpKQo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1vcHRpb24KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wdGlvbiAiZm9yIGxvY2FsIikpKSkKPiArwqDCoMKg wqDCoMKgwqDCoMKgwqDCoCAoYWN0aW9uCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3Bl bnNtdHBkLXJlbGF5Cj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChuYW1lICJyZWxheSIp KSkpKSkKPiArCj4gKzs7IGR1cGxpY2F0ZSBmcm9tcwo+ICsoZGVmaW5lIChiYWQtbWF0Y2gzKQo+ ICvCoCAoY3JlYXRlLWJhZC1yZWNvcmQKPiArwqDCoCAob3BlbnNtdHBkLW1hdGNoCj4gK8KgwqDC oCAob3B0aW9ucyAobGlzdAo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAob3BlbnNtdHBk LW9wdGlvbgo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcHRpb24gImZyb20gYW55 IikpCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcGVuc210cGQtb3B0aW9uCj4gK8Kg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wdGlvbiAiZnJvbSBhdXRoIikpKSkKPiArwqDC oMKgIChhY3Rpb24KPiArwqDCoMKgwqAgKG9wZW5zbXRwZC1yZWxheQo+ICvCoMKgwqDCoMKgIChu YW1lICJyZWxheSIpKSkpKSkKPiArCj4gKzs7IHJjcHQtdG8gbXVzdCBoYXZlIGEgZGF0YSBmaWVs ZC4KPiArKGRlZmluZSAoYmFkLW1hdGNoNCkKPiArwqAgKGNyZWF0ZS1iYWQtcmVjb3JkCj4gK8Kg wqAgKG9wZW5zbXRwZC1tYXRjaAo+ICvCoMKgwqAgKG9wdGlvbnMgKGxpc3QKPiArwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1vcHRpb24KPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoCAob3B0aW9uICJyY3B0LXRvIikpKSkKPiArwqDCoMKgIChhY3Rpb24KPiArwqDC oMKgwqAgKG9wZW5zbXRwZC1yZWxheQo+ICvCoMKgwqDCoMKgIChuYW1lICJyZWxheSIpKSkpKSkK PiArCj4gKzs7IG9wdGlvbiAndGxzJyBjYW5ub3QgaGF2ZSBmaWVsZG5hbWUKPiArOzsgJ2RhdGEn IGRlZmluZWQuCj4gKyhkZWZpbmUgKGJhZC1tYXRjaDUpCj4gK8KgIChjcmVhdGUtYmFkLXJlY29y ZAo+ICvCoMKgIChvcGVuc210cGQtbWF0Y2gKPiArwqDCoMKgIChvcHRpb25zIChsaXN0Cj4gK8Kg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChvcGVuc210cGQtb3B0aW9uCj4gK8KgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqAgKG9wdGlvbiAidGxzIikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoCAoZGF0YSAiaGVsbG8iKSkpKQo+ICvCoMKgwqAgKGFjdGlvbgo+ICvCoMKgwqDCoCAo b3BlbnNtdHBkLXJlbGF5Cj4gK8KgwqDCoMKgwqAgKG5hbWUgInJlbGF5IikpKSkpKQo+ICsKPiAr OzsgZm9yIGFueSBjYW5ub3QgaGF2ZSBkYXRhCj4gKzs7IG9yIHJlZ2V4IGRlZmluZWQKPiArKGRl ZmluZSAoYmFkLW1hdGNoNikKPiArwqAgKGNyZWF0ZS1iYWQtcmVjb3JkCj4gK8KgwqAgKG9wZW5z bXRwZC1tYXRjaAo+ICvCoMKgwqAgKG9wdGlvbnMgKGxpc3QKPiArwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqAgKG9wZW5zbXRwZC1vcHRpb24KPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCAob3B0aW9uICJmb3IgYW55IikKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAocmVn ZXggI3QpKSkpCj4gK8KgwqDCoCAoYWN0aW9uCj4gK8KgwqDCoMKgIChvcGVuc210cGQtcmVsYXkK PiArwqDCoMKgwqDCoCAobmFtZSAicmVsYXkiKSkpKSkpCj4gKwo+ICs7OyBtYXRjaCBuZWVkcyBh biBhY3Rpb24KPiArKGRlZmluZSAoYmFkLW1hdGNoNykKPiArwqAgKGNyZWF0ZS1iYWQtcmVjb3Jk Cj4gK8KgwqAgKG9wZW5zbXRwZC1tYXRjaAo+ICvCoMKgwqAgKG9wdGlvbnMgKGxpc3QKPiArwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKG9wZW5zbXRwZC1vcHRpb24KPiArwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoCAob3B0aW9uICJmcm9tIGF1dGgiKSkpKSkpKQo+ICsKPiArKGRlZmlu ZSAocnVuLW9wZW5zbXRwZC1yZWNvcmQtc2FuaXRhdGlvbi10ZXN0KQo+ICvCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqAgOyh3aXRoLW91dHB1dC10by1wb3J0Cj4gKCVtYWtlLXZvaWQtcG9ydCAidyIpCj4g K8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCA7wqAgKGxhbWJkYSAoKQo+ICvCoCAodGVzdC1iZWdpbiAi cnVuLW9wZW5zbXRwZC1yZWNvcmQtc2FuaXRhdGlvbi10ZXN0IikKPiArCj4gK8KgIDs7IFRPRE8g Zml4IG1lIQo+ICvCoCAodGVzdC1hc3NlcnQgIlRlc3QgPGludGVyZmFjZT4gZmllbGRuYW1lICdm aWx0ZXJzJyBoYXMgdHdvIGZpbHRlcnMKPiB3aXRoIHRoZSBzYW1lIG5hbWUuIgo+ICvCoMKgwqAg KGNhdGNoICN0IGJhZC1pbnRlcmZhY2UxIHJldHVybi10cnVlKSkKPiArCj4gK8KgICh0ZXN0LWFz c2VydCAiVGVzdCA8aW50ZXJmYWNlPiBjYW5ub3QgaGF2ZSB0d28gZmlsdGVycyB3aXRoIHRoZQo+ IHNhbWUgbmFtZS4iCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGNhdGNoICN0IGJh ZC1pbnRlcmZhY2UyIHJldHVybi10cnVlKSkKPiArCj4gK8KgICh0ZXN0LWFzc2VydCAiVGVzdCA8 ZmlsdGVyLXBoYXNlPiBmaWVsZG5hbWUgJ3BoYXNlJyB0aGUgcmlnaHQKPiBzdHJpbmcuIgo+ICvC oMKgwqAgKGNhdGNoICN0IGJhZC1maWx0ZXItcGhhc2UxIHJldHVybi10cnVlKSkKPiArCj4gK8Kg ICh0ZXN0LWFzc2VydCAiVGVzdCA8ZmlsdGVyLXBoYXNlPiBmaWVsZG5hbWUgJ2RlY2lzaW9uJyB3 LyB2YWx1ZQo+IFwicmVqZWN0XCIgYW5kIFwiZGlzY29ubmVjdFwiIHJlcXVpcmVzIGEgJ21lc3Nh Z2UnLiIKPiArwqDCoMKgIChjYXRjaCAjdCBiYWQtZmlsdGVyLXBoYXNlMiByZXR1cm4tdHJ1ZSkp Cj4gKwo+ICvCoCAodGVzdC1hc3NlcnQgKHN0cmluZy1hcHBlbmTCoCAiVGVzdCA8ZmlsdGVyLXBo YXNlPiBmaWVsZG5hbWUKPiAnZGVjaXNpb24nICIKPiArwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJ3LyB2YWx1ZSBcInJlamVjdFwi IGFuZAo+IFwiZGlzY29ubmVjdFwiIHJlcXVpcmVzIGEgJ21lc3NhZ2UnLiIKPiArwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICIgVGhl IG1lc3NhZ2UgbXVzdCBiZWdpbiB3aXRoIDR4eCBvcgo+IDV4eC4iKQo+ICvCoMKgwqAgKGNhdGNo ICN0IGJhZC1maWx0ZXItcGhhc2UzIHJldHVybi10cnVlKSkKPiArCj4gK8KgICh0ZXN0LWFzc2Vy dCAiVGVzdCA8ZmlsdGVyLXBoYXNlPiBmaWVsZG5hbWUgJ3Jld3JpdGUnIHJlcXVpcmVzCj4gZmll bGRuYW1lICd2YWx1ZScgdG8gaGF2ZSBhIG51bWJlci4iCj4gK8KgwqDCoCAoY2F0Y2ggI3QgYmFk LWZpbHRlci1waGFzZTQgcmV0dXJuLXRydWUpKQo+ICsKPiArwqAgKHRlc3QtYXNzZXJ0IChzdHJp bmctYXBwZW5kICJUZXN0IDxmaWx0ZXItcGhhc2U+IGZpZWxkbmFtZQo+ICdkZWNpc2lvbicgd2l0 aCB2YWx1ZXMgJ2p1bmsnIG9yICdieXBhc3MnLCAiCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJ0aGVuIGZpZWxkbmFtZSAnbWVzc2Fn ZScgYW5kICd2YWx1ZScKPiBtdXN0IGJlIGJsYW5rLiIpCj4gK8KgwqDCoCAoY2F0Y2ggI3QgYmFk LWZpbHRlci1waGFzZTUgcmV0dXJuLXRydWUpKQo+ICsKPiArwqAgKHRlc3QtYXNzZXJ0ICJZb3Ug Y2Fubm90IGp1bmsgYW4gZW1haWwgb24gcGhhc2UgY29tbWl0LiIKPiArwqDCoMKgIChjYXRjaCAj dCBiYWQtZmlsdGVyLXBoYXNlNiByZXR1cm4tdHJ1ZSkpCj4gKwo+ICvCoCA7OyBUT0RPIGZpeCBt ZSEKPiArwqAgKHRlc3QtYXNzZXJ0ICJUZXN0IDxmaWx0ZXItcGhhc2U+IGhhcyAyIGR1cGxpY2F0 ZSBvcHRpb25zLiIKPiArwqDCoMKgIChjYXRjaCAjdCBiYWQtZmlsdGVyLXBoYXNlNyByZXR1cm4t dHJ1ZSkpCj4gKwo+ICvCoCAodGVzdC1hc3NlcnQgIlRlc3QgPGZpbHRlci1waGFzZT4gb3B0aW9u ICdzcmMnIHJlcXVpcmVzIGEgdGFibGUuIgo+ICvCoMKgwqAgKGNhdGNoICN0IGJhZC1maWx0ZXIt cGhhc2U4IHJldHVybi10cnVlKSkKPiArCj4gK8KgIDs7IFRPRE8gZml4IG1lIQo+ICvCoCAodGVz dC1hc3NlcnQgIlRlc3QgPGZpbHRlci1waGFzZT4gb3B0aW9uICdmY3JkbnMnIGNhbm5vdCBoYXZl IGEKPiB0YWJsZS4iCj4gK8KgwqDCoCAoY2F0Y2ggI3QgYmFkLWZpbHRlci1waGFzZTkgcmV0dXJu LXRydWUpKQo+ICsKPiArwqAgKHRlc3QtYXNzZXJ0ICJUZXN0IDxvcGVuc210cGQtbWF0Y2g+IGZp ZWxkbmFtZSAnb3B0aW9ucycgc2hvdWxkCj4gbm90IGJlIHF1b3RlZC4iCj4gK8KgwqDCoCAoY2F0 Y2ggI3QgYmFkLW1hdGNoMSByZXR1cm4tdHJ1ZSkpCj4gKwo+ICvCoCAodGVzdC1hc3NlcnQgIlRl c3QgPG9wZW5zbXRwZC1tYXRjaD4gaGFzIGR1cGxpY2F0ZSAnZm9yJyBvcHRpb25zLiIKPiArwqDC oMKgIChjYXRjaCAjdCBiYWQtbWF0Y2gyIHJldHVybi10cnVlKSkKPiArCj4gK8KgICh0ZXN0LWFz c2VydCAiVGVzdCA8b3BlbnNtdHBkLW1hdGNoPiBoYXMgZHVwbGljYXRlICdmcm9tJwo+IG9wdGlv bnMuIgo+ICvCoMKgwqAgKGNhdGNoICN0IGJhZC1tYXRjaDMgcmV0dXJuLXRydWUpKQo+ICsKPiAr wqAgKHRlc3QtYXNzZXJ0ICJUZXN0IDxvcGVuc210cGQtbWF0Y2g+IG9wdGlvbiAncmNwdCcgbXVz dCBoYXZlCj4gZGF0YS4iCj4gK8KgwqDCoCAoY2F0Y2ggI3QgYmFkLW1hdGNoNCByZXR1cm4tdHJ1 ZSkpCj4gKwo+ICvCoCAodGVzdC1hc3NlcnQgIlRlc3QgPG9wZW5zbXRwZC1tYXRjaD4gb3B0aW9u ICd0bHMnIGNhbm5vdCBoYXZlCj4gZmllbGRuYW1lICdkYXRhJyBkZWZpbmVkLiIKPiArwqDCoMKg IChjYXRjaCAjdCBiYWQtbWF0Y2g1IHJldHVybi10cnVlKSkKPiArCj4gK8KgICh0ZXN0LWFzc2Vy dCAiVGVzdCA8b3BlbnNtdHBkLW1hdGNoPiBvcHRpb24gJ2ZvciBhbnknIGNhbm5vdCBoYXZlCj4g ZmllbGRuYW1lICdkYXRhJyBkZWZpbmVkLiIKPiArwqDCoMKgIChjYXRjaCAjdCBiYWQtbWF0Y2g2 IHJldHVybi10cnVlKSkKPiArCj4gK8KgICh0ZXN0LWFzc2VydCAiVGVzdCA8b3BlbnNtdHBkLW1h dGNoPiBuZWVkcyBmaWVsZG5hbWUgJ2FjdGlvbicKPiBuZWVkcyB0byBiZSBkZWZpbmVkLiIKPiAr wqDCoMKgIChjYXRjaCAjdCBiYWQtbWF0Y2g3IHJldHVybi10cnVlKSkKPiArCj4gK8KgICh0ZXN0 LWVuZCAicnVuLW9wZW5zbXRwZC1yZWNvcmQtc2FuaXRhdGlvbi10ZXN0IikpCj4gKwo+ICsoZGVm aW5lICV0ZXN0LW9wZW5zbXRwZC1yZWNvcmQtc2FuaXRhdGlvbgo+ICvCoCAoc3lzdGVtLXRlc3QK PiArwqDCoCAobmFtZSAib3BlbnNtdHBkUmVjb3JkU2FuaXRhdGlvbiIpCj4gK8KgwqAgKGRlc2Ny aXB0aW9uCj4gK8KgwqDCoCAoc3RyaW5nLWFwcGVuZCAiPG9wZW5zbXRwZD4gaGFzIG51bWVyb3Vz IHNhbml0eSBjaGVja3MuXG4iCj4gK8KgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oCAiVGhpcyBjaGVja3MgdGhhdCBpbnZhbGlkIGNvbmZpZ3VyYXRpb25zLCByZXR1cm4KPiBhblxu Igo+ICvCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgImFwcHJvcHJpYXRlIGVy cm9yLlxuIikpCj4gK8KgwqAgKHZhbHVlIChydW4tb3BlbnNtdHBkLXJlY29yZC1zYW5pdGF0aW9u LXRlc3QpKSkpCj4gKwo+IMKgCj4gwqAoZGVmaW5lICVleGltLW9zCj4gwqDCoCAoc2ltcGxlLW9w ZXJhdGluZy1zeXN0ZW0KPiAKPiBiYXNlLWNvbW1pdDogNGIzNDkzZWQwMTU2NzA5YTkyNGYzMWVm NGM5YTVlZmEwODE1ZGZlOAoKQ2hlZXJzCg== From debbugs-submit-bounces@debbugs.gnu.org Mon Oct 24 18:18:51 2022 Received: (at 56046) by debbugs.gnu.org; 24 Oct 2022 22:18:51 +0000 Received: from localhost ([127.0.0.1]:49697 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1on5mR-0003r7-1R for submit@debbugs.gnu.org; Mon, 24 Oct 2022 18:18:50 -0400 Received: from mx1.dismail.de ([78.46.223.134]:31561) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1on5mL-0003qa-0o for 56046@debbugs.gnu.org; Mon, 24 Oct 2022 18:18:45 -0400 Received: from mx1.dismail.de (localhost [127.0.0.1]) by mx1.dismail.de (OpenSMTPD) with ESMTP id d58a012d; Tue, 25 Oct 2022 00:18:33 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; d=dismail.de; h= mime-version:date:content-type:content-transfer-encoding:from :message-id:subject:to:in-reply-to:references; s=20190914; bh=Rt eCSvTqvn1GO6Lj+CIRMmCvCvb5RT/9LzKHnAH19wk=; b=hd+qh8780WA+b7gnhN a09dVpy0DPBQBpXkuv4eroM5AGGSQSYIDZt1XKzvbWq5q4VCnKRGD0/nGPVeDtsL kOsV/62H1wI4euOvp5nFZDtlTWFAKCGXvfK8t4E5kc31ipIHg4kpVd4YOVX001zx g0Tb1vdgCPx06jFPGnPR137Q/jTxf+Rf8nMcZpXbkJIvAIMYmt3DhC8VhuavsJBc E6juyd7jaTWVgp3ydY7stcF6UdBghtUuQbTqW03FVgpD4ZPGQSNPAgAl7UA3tKM/ yVhKWkqx73nb02h1nVk+9RGsJ7y/gd8R8ln5MXVwjNK5VEhL5EY9vDhT7LQohYvN Uv6g== Received: from smtp2.dismail.de ( [10.240.26.12]) by mx1.dismail.de (OpenSMTPD) with ESMTP id c5cc63bc; Tue, 25 Oct 2022 00:18:33 +0200 (CEST) Received: from smtp2.dismail.de (localhost [127.0.0.1]) by smtp2.dismail.de (OpenSMTPD) with ESMTP id 5eb3b407; Tue, 25 Oct 2022 00:18:33 +0200 (CEST) Received: by dismail.de (OpenSMTPD) with ESMTPSA id 03f50fdf (TLSv1.3:TLS_AES_256_GCM_SHA384:256:NO); Tue, 25 Oct 2022 00:18:33 +0200 (CEST) MIME-Version: 1.0 Date: Mon, 24 Oct 2022 22:18:32 +0000 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable X-Mailer: RainLoop/1.16.0a From: jbranso@dismail.de Message-ID: <04366ffca8f1b8648acd77ae0ddb0a80@dismail.de> Subject: Re: [Patch master v2] services (opensmtpd): add opensmtpd records to enhance opensmtpd-configuration. To: "Liliana Marie Prikler" , 56046@debbugs.gnu.org In-Reply-To: References: X-Spam-Score: -0.7 (/) X-Debbugs-Envelope-To: 56046 X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.7 (-) October 24, 2022 2:29 PM, "Liliana Marie Prikler" wrote: > Am Montag, dem 24.10.2022 um 13:30 -0400 schrieb Joshua Branson: >=20 >=20Cheers Thanks for the speedy response and quick review! I also forgot to mention that the service is actually fairly use-able now= ! takes in a list of strings and gexps. With a little work you can use s-expressions to configure dkimsigning. And it should be possible to integrate bogofilter, but I have not figured that out yet..... Thanks, Joshua From debbugs-submit-bounces@debbugs.gnu.org Fri Dec 23 10:18:01 2022 Received: (at 56046) by debbugs.gnu.org; 23 Dec 2022 15:18:01 +0000 Received: from localhost ([127.0.0.1]:37308 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1p8jo9-00062T-B8 for submit@debbugs.gnu.org; Fri, 23 Dec 2022 10:18:01 -0500 Received: from smtp.gnucode.me ([45.56.66.20]:45396 helo=localhost) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1p8iT8-0004qf-BS for 56046@debbugs.gnu.org; Fri, 23 Dec 2022 08:52:14 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; s=2021-09-22; bh=xzOLG/D 4CjLrKSSeUaOvbtZJ68gZX+akGdzBTHvrs3M=; h=references:in-reply-to:date: subject:cc:to:from; d=gnucode.me; b=QUcFR/n++JBo6IUf/hDwQkv7BNK1JnASeA eWRSTpgWl96XqhEPjQZlJsDnxfy07A1F/d8AaXhedNpaw1sBq+swDz0zCzTQDsaHgYUqyd kLL4VZMBOBUwjOtIGQyGQbofoRyiUunew+GZsH2NiHhCDeCUs30GVxzMwLZk1OH3e84= Received: from crazyhorse.tds (h134-215-52-79.cytnin.broadband.dynamic.tds.net [134.215.52.79]) by localhost (OpenSMTPD) with ESMTPSA id 5702ec26 (TLSv1.3:TLS_AES_256_GCM_SHA384:256:NO); Fri, 23 Dec 2022 13:52:07 +0000 (UTC) From: Joshua Branson To: 56046@debbugs.gnu.org Subject: [PATCH opensmtpd-records v3] services (opensmtpd): add opensmtpd records to enhance opensmtpd-configuration. Date: Fri, 23 Dec 2022 08:52:03 -0500 Message-Id: <8001f404c0e54baba71ef9c5536abc54275c4167.1671803496.git.joshua@gnucode.me> X-Mailer: git-send-email 2.38.1 In-Reply-To: References: MIME-Version: 1.0 Content-Type: text/plain; charset=y Content-Transfer-Encoding: 8bit X-Debbugs-Envelope-To: 56046 X-Mailman-Approved-At: Fri, 23 Dec 2022 10:17:59 -0500 Cc: liliana.prikler@gmail.com, Joshua Branson X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" From: Joshua Branson This is V3 of my opensmtp records configuration. Hopefully soon, I will have this WIP service is guixrus, so more people can easily play with it. https://git.sr.ht/~whereiseveryone/guixrus BUT the new milestone is this, I am now actually able to use opensmtpd on my gnucode.me account! So that is pretty awesome! Here is a link to my server config: https://notabug.org/jbranso/linode-guix-system-configuration/src/master/linode-locke-lamora-current-config.scm The best place to find documentation for this service is here: https://notabug.org/jbranso/linode-guix-system-configuration/src/master/opensmtpd-records-documentation.txt Openmstpd-configuration may only be configured by a config-file that uses the smtpd.conf syntax. This patch, enables one to configure opensmtpd by using record types. * gnu/services/mail.scm: (opensmtpd-table, opensmtpd-ca, opensmtpd-pki, opensmtpd-action-local-delivery, opensmtpd-maildir, opensmtpd-mda, opensmtpd-lmtp, opensmtpd-relay, opensmtpd-option, opensmtpd-filter-phase, opensmtpd-filter, opensmtpd-interface, opensmtpd-socket, opensmtpd-match, opensmtpd-smtp, opensmtpd-srs, opensmtpd-queue, and opensmtpd-configuration): New records. (false?, is-value-right-type, add-comma-or-string, list-of-procedures->string, string-in-list?, my-sanitize, opensmtpd-filter-chain?, throw-error-duplicate-option, sanitize-list-of-options-for-match, sanitize-filters, list-has-duplicates-or-non-filters?, filter-phase-has-message-and-value?, filter-phase-decision-lacks-proper-message?, filter-phase-lacks-proper-value?, filter-phase-has-incorrect-junk-or-bypass?, filter-phase-junks-after-commit?, list-of-unique-filter-or-filter-phase?, throw-error, contains-duplicate?, list-of-type?, list-of-strings?, list-of-unique-opensmtpd-option?, list-of-opensmtpd-ca?, list-of-opensmtpd-pki?, list-of-opensmtpd-listen-on?, list-of-unique-opensmtpd-match?, list-of-strings->string, assoc-list? assoc-list, variable->string, tables-data-are-assoc-list?, tables-data-are-a-list-of-strings?, table-data-are-a-nested-list-of-strings?, assoc-list->string, opensmtpd-table->string, opensmtpd-listen-on->string, opensmtpd-listen-on-socket->string, opensmtpd-action-relay->string, opensmtpd-lmtp->string, opensmtpd-mda->string, opensmtpd-maildir->string, opensmtpd-action-local-delivery->string, opensmtpd-action->string, opensmtpd-option->string, opensmtpd-match->string, opensmtpd-ca->string, opensmtpd-pki->string, generate-filter-chain-name, opensmtpd-filter-chain->string, opensmtpd-filter-phase->string, opensmtpd-filters->string, opensmtpd-listen->string, opensmtpd-srs->string, opensmtpd-smtp->string, opensmtpd-queue->string, get-opensmtpd-actions, get-opensmtpd-pkis, get-opensmtpd-filters, flatten, get-opensmtpd-tables, opensmtpd-fieldname->string, list-of-records->string, opensmtpd->mixed-text-file): New procedures. * gnu/tests/mail.scm : new tests for various opensmtpd records. * doc/guix.texi (OpenSMTPD Service): Added documentation for the new records for opensmtpd. --- doc/guix.texi | 1065 ++++++++++++++++- gnu/services/mail.scm | 2560 ++++++++++++++++++++++++++++++++++++++++- gnu/tests/mail.scm | 713 ++++++++++++ 3 files changed, 4310 insertions(+), 28 deletions(-) diff --git a/doc/guix.texi b/doc/guix.texi index 535c8cdfc3..879a2ad233 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -25407,16 +25407,66 @@ could instantiate a dovecot service like this: @end lisp @subsubheading OpenSMTPD Service +@cindex opensmtpd @deffn {Scheme Variable} opensmtpd-service-type -This is the type of the @uref{https://www.opensmtpd.org, OpenSMTPD} -service, whose value should be an @code{opensmtpd-configuration} object -as in this example: +OpenSMTPD is an easy-to-use mail transfer agent (MTA). OpenSMTPD +@strong{listens} for incoming mail and @strong{matches} the mail to +@strong{actions}. The following records represent those stages: -@lisp -(service opensmtpd-service-type - (opensmtpd-configuration - (config-file (local-file "./my-smtpd.conf")))) +@multitable {aaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @strong{listens} +@tab @code{} +@item +@tab @code{} +@item +@tab +@item @strong{matches} +@tab @code{} +@item +@tab +@item @strong{actions} +@tab @code{} +@item +@tab @code{} +@end multitable + +Additionally, each @code{} and +@code{} may use a list of @code{}, +and/or @code{} records to filter +email/spam. Also numerous records' fieldnames use +@code{} records to hold lists or key value pairs of +data. Be sure to read the @code{} section to learn the +differance between a @code{mapping table} and a @code{list table}. + +Finally, both @code{} and +@code{} use @code{} to +configure various options. + +A simple example opensmtpd configuration is below: + +@lisp +(let ((smtp.gnu.org (opensmtpd-pki + (domain "smtp.gnu.org") + (cert "file.cert") + (key "file.key")))) + (service opensmtpd-service-type + (opensmtpd-configuration + (interfaces (list + (opensmtpd-interface + (pki smtp.gnu.org)) + (opensmtpd-interface + (pki smtp.gnu.org) + (secure-connection "smtps")))) + (matches (list + (opensmtpd-match + (action + (opensmtpd-local-delivery + (name "local-delivery")))) + (opensmtpd-match + (action + (opensmtpd-relay + (name "relay"))))))))) @end lisp @end deffn @@ -25425,7 +25475,7 @@ Data type representing the configuration of opensmtpd. @table @asis @item @code{package} (default: @var{opensmtpd}) -Package object of the OpenSMTPD SMTP server. +Package object of the OpenSMTPD server. @item @code{config-file} (default: @code{%default-opensmtpd-config-file}) File-like object of the OpenSMTPD configuration file to use. By default @@ -25433,14 +25483,1013 @@ it listens on the loopback network interface, and allows for mail from users and daemons on the local machine, as well as permitting email to remote servers. Run @command{man smtpd.conf} for more information. +@item @code{bounce} (default: @code{(list "4h")}) +@code{bounce} is a list of strings, which send warning messages to the +envelope sender when temporary delivery failures cause a message to +remain in the queue for longer than a specified delay. Each delay option +is a string parameter beginning with a positive decimal integer and a +unit, which can be 's', 'm', 'h', or 'd'. At most four delay parameters +can be specified. + +@item @code{interfaces} default: +@lisp +(list + (opensmtpd-interface + (interface "lo") + (port 25))) +@end lisp +@code{interfaces} is a list of @code{} records. +This list details what interfaces and ports OpenSMTPD listens on as well as +other options. + +@item @code{socket} (default: @code{(opensmtpd-socket)}) +Listens for incoming connections on the Unix domain socket. + +@item @code{includes} (default: @code{#f}) +@code{includes} is a list of string filenames. Each filename's contents is +additional configuration that is inserted into the top of the configuration +file. Run @code{man smtpd.conf} for more information. + +@item @code{matches} default: +@lisp +(list (opensmtpd-match + (action (opensmtpd-local-delivery + (name "local") + (method "mbox") + (options + (list + (opensmtpd-option + (option "for local"))))))) + (opensmtpd-match + (action (opensmtpd-relay + (name "outbound"))) + (options + (list + (opensmtpd-option + (option "from local")) + (opensmtpd-option + (option "for any")))))) +@end lisp +@code{matches} is a list of @code{} records, which +matches incoming mail and sends it to a correspending action. The match +records are evaluated sequentially, with the first match winning. +Therefore @emph{the order that you arrange your matches is important}. +It's a good idea to put specific matches first and an all emcompassing +match (like @code{(option "for any")}) @strong{last}. If an incoming +mail does not match any match records, then it is rejected. + +@item @code{mta-max-deferred} (default: @code{100}) +When delivery to a given host is suspended due to temporary failures, cache +at most number envelopes for that host such that they can be delivered as +soon as another delivery succeeds to that host. The default is 100. + +@item @code{queue} (default: @code{#f}) +@code{queue} expects an @code{} record. With it, one may +compress and encrypt queue-ed emails as well as set the default expiration +time for temporarily undeliverable messages. + +@item @code{smtp} (default: @code{#f}) +@code{smtp} expects an @code{} record, which lets one +specifiy how large email may be along with other settings. + +@item @code{srs} (default: @code{#f}) +@code{srs} expects an @code{} record, which lets one set +up SRS, the Sender Rewritting Scheme. + @item @code{setgid-commands?} (default: @code{#t}) Make the following commands setgid to @code{smtpq} so they can be executed: @command{smtpctl}, @command{sendmail}, @command{send-mail}, @command{makemap}, @command{mailq}, and @command{newaliases}. @xref{Setuid Programs}, for more information on setgid programs. + @end table @end deftp +@itemize +@item Data Type: opensmtpd-interface +Data type representing the configuration of an +@code{}. It listens on the fieldname +@code{interface} for incoming connections, using the same syntax as +@code{ifconfig}. The interface parameter may also be an string interface +group, an string IP address, or a string domain name. Listening can +optionally be restricted to a specific address via the fieldname +@code{family}, which can be either @code{"inet4"} or @code{"inet6"}. + +@itemize +@item @code{interface} (default: @code{"lo"}) + +The string interface to listen for incoming connections. This string +may be an interface group, an IP address, or a domain name. These +interfaces can usually be found by the command @code{ip link}. + +@item @code{family} (default: @code{#f}) + +Only listen on a specific address family. Valid strings are +@code{"inet4"} or @code{"inet6"}, which will only listen on IPv4 or IPv6 +respectfully. If @code{(family #f)}, then opensmtpd will listen on both +IPv4 and IPv6. + +@item @code{auth} (default: @code{#f}) +Support SMTPAUTH: clients may only start SMTP transactions after +successful authentication. If @code{auth} is @code{#t}, then users are +authenticated against their own normal login credentials. Alternatively +@code{auth} may be a @code{mapping table} that maps usernames to +encrypted passwords. The password can be encrypted via the +@code{smtpctl} @code{encrypt} subcommand. + +@item @code{auth-optional} (default: @code{#f}) +Support SMTPAUTH optionally: clients need not authenticate, but may do +so. This allows the @code{} to both accept +incoming mail from untrusted senders and permit outgoing mail from +authenticated users. It can be used in situations where it is not +possible to listen on a separate port (usually the submission port, 587) +for users to authenticate. This option also accepts a @code{mapping +table} that maps usernames to encrypted passwords. + +@item @code{filters} (default: @code{#f}) +A list of one or many @code{} or +@code{} records. The filters are applied +sequentially. These records listen and filter on connections handled by this +listener. + +@item @code{hostname} (default: @code{#f}) +Change the default server name in the greeting banner instead of the +default one. + +@item @code{hostnames} (default: @code{#f}) +Override the server name for specific addresses. Use a @code{mapping +table} that maps string IP addresses to string hostnames. If the address +on which the connection arrives appears in the mapping, the associated +hostname is used. + +@item @code{mask-src} (default: @code{#f}) +If @code{#t}, then omit the from part when prepending “Received” headers. + +@item @code{disable-dsn} (default: @code{#f}) +When @code{#t}, then disable the DSN (Delivery Status Notification) extension. + +@item @code{pki} (default: @code{#f}) +For secure connections, use an @code{} record to prove a +mail server's identity. + +@item @code{port} (default: @code{25}) +Listen on the integer port instead of the default port of 25. + +@item @code{proxy-v2} (default: @code{#f}) +If @code{#t}, then support the PROXYv2 protocol, rewriting appropriately source +address received from proxy. + +@item @code{received-auth} (default: @code{#f}) +If @code{#t}, then in “Received” headers, report whether the session was +authenticated and by which local user. + +@item @code{senders} (default: @code{#f}) +Look up the authenticated user in the supplied @code{mapping table} to +find the email addresses that user is allowed to submit mail as. + +@item @code{masquerade} (default: @code{#f}) +@code{masquerade}, is used in conjunction with @code{senders}. If +@code{#t}, then the From header is rewritten to match the sender +provided in the SMTP session. If @code{senders} is @code{#false}, then +@code{masquerade} cannot be @code{#t}. + +@item @code{secure-connection} (default: @code{#f}) +This is a string of one of these options: + +@multitable {aaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @code{"smtps"} +@tab Support SMTPS, by default on port 465. +@item @code{"tls"} +@tab Support STARTTLS, by default on port 25. +@item @code{"tls-require"} +@tab Like @code{"tls"}, but force clients to +@item +@tab establish a secure connection before being +@item +@tab allowed to start an SMTP transaction. +@item @code{"tls-require-verify"} +@tab Like @code{"tls-require"}, but clients must +@item +@tab also provide a valid certificate +@item +@tab to establish an SMTP session. +@end multitable + +@item @code{tag} (default: @code{#f}) +Clients connecting to the listener are tagged with the given string tag. +@end itemize + +@item Data Type: opensmtpd-socket +Data type representing the configuration of an +@code{}. Listen for incoming SMTP connections on the +Unix domain socket @samp{/var/run/smtpd.sock}. This is done by default, +even if the record is absent. + +@itemize +@item @code{filters} (default: @code{#f}) +A list of one or many @code{} or +@code{} records. These filter incoming +connections handled by this listener. + +@item @code{mask-src} (default: @code{#f}) +If @code{#t}, then omit the from part when prepending “Received” headers. + +@item @code{tag} (default: @code{#f}) +Clients connecting to the listener are tagged with the given string tag. +@end itemize + +@item Data Type: opensmtpd-match +@cindex opensmtpd-match +This data type represents the configuration of an +@code{} record. + +If at least one mail envelope matches the options of one match record, +receive the incoming message, put a copy into each matching envelope, +and atomically save the envelopes to the mail spool for later processing +by the respective @code{} found in fieldname +@code{action}. Here is an example @code{opensmtpd-match} +record. + +@lisp +(opensmtpd-match + (action (opensmtpd-local-delivery + (name "receive") + (method (opensmtpd-maildir + (pathname "/home/%@{rcpt.user@}/Maildir") + (junk #t))) + (virtual (opensmtpd-table + (name "virt") + (data '(("carmen" . "carmen@@gnu.org"))))))) + (options (list (opensmtpd-option + (option "from any")) + (opensmtpd-option + (option "for domain") + (data (opensmtpd-table + (name "domain-table") + (data (list "gnu.org" "fsf.org")))))))) +@end lisp + +@itemize +@item @code{action} (default: @code{#f}) + +If mail matches this match configuration, then do this action. Valid values +include @code{} or +@code{}. + +@item @code{options} (default: @code{#f}) +The fieldname @code{option} is a list of unique +@code{} records. + +There are some mutually exclusive options: there can be only one ``for'' +and only one ``from'' option. + +@multitable {aaaaaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@headitem for +@tab from +@item only use one of the following: +@tab only use one of the following: +@item @code{"for any"} +@tab @code{"from any"} +@item @code{"for local"} +@tab @code{"from auth"} +@item @code{"for domain"} +@tab @code{"from local"} +@item @code{"for rcpt-to"} +@tab @code{"from mail-from"} +@item +@tab @code{"from socket"} +@item +@tab @code{"from src"} +@end multitable + +Additionally, some options require additional data via +@code{}'s fieldname @code{data}. The following list +will explain the below syntax. + +@itemize +@item @code{"for any"} +This option only requires fieldname @code{option} to have the string +@code{"for any"}: + +@lisp + (opensmtpd-option + (option "for any")) +@end lisp + +@item @code{"tag"} _tag_ +This option only requires fieldname @code{option} to have the string +@code{"tag"} with a string in fieldname @code{data}: + +@lisp + (opensmtpd-option + (option "tag") + (data "this-tag")) +@end lisp + +@item @code{"for rcpt"} _domain_ | +This option requires fieldname @code{data} to have a string domain or +@code{list table}: + +@lisp + (opensmtpd-option + (option "for rcpt") + (data "gnu.org")) +@end lisp + +OR + +@lisp + (opensmtpd-option + (option "for rcpt") + (data (list "gnu.org" "fsf.org"))) +@end lisp +@end itemize + +The following matching options are supported and can all be negated (via not +#t). The options that support a table (anything surrounded with '<' and '>' +eg:
), also support specifying regex via (regex #t). + +@itemize +@item @code{"for any"} +Specify that session may address any destination. + +@item @code{"for local"} +Specify that session may address any local domain. This is the default, +and may be omitted. + +@item @code{"for domain"} _domain_ | +Specify that session may address the string _domain_ or +@code{list table} . + +@item @code{"for rcpt-to"} _recipient_ | +Specify that session may address the string _recipient_ or list table +. + +@item @code{"from any"} +Specify that session may originate from any source. + +@item @code{"from auth"} +Specify that session may originate from any authenticated user, no matter +the source IP address. + +@item @code{"from auth"} _user_ | +Specify that the session may originate from authenticated _user_ or +@code{list table} , no matter the source IP address. + +@item @code{"from local"} +Specify that session may only originate from a local IP address, or from +the local enqueuer. This is the default, and may be omitted. + +@item @code{"from mail-from"} _sender_ | +Specify that session may originate from _sender_ or @code{list table} +, no matter the source IP address. + +@item @code{"from rdns"} +Specify that session may only originate from an IP address that resolves +to a reverse DNS@. + +@item @samp{"from rdns"} _hostname_ | +Specify that session may only originate from an IP address that resolves +to a reverse DNS matching string _hostname_ or @code{list table} +. + +@item @samp{"from socket"} +Specify that session may only originate from the local enqueuer. + +@item @code{"from src"} _address_ |
+Specify that session may only originate from string _address_ or +@code{list table}
which can be a specific address or a subnet +expressed in CIDR-notation. + +@item @code{"auth"} +Matches transactions which have been authenticated. + +@item @code{"auth"} _username_ | +Matches transactions which have been authenticated for string _user_ or +@code{list table} . + +@item @code{"helo"} _helo-name_ | +Specify that session's HELO / EHLO should match the string _helo-name_ +or @code{list table} . + +@item @code{"mail-from"} _sender_ | +Specify that transactions's MAIL FROM should match the string _sender_ +or @code{list table} . + +@item @code{"rcpt-to"} _recipient_ | +Specify that transaction's RCPT TO should match the string _recipient_ +or @code{list table} . + +@item @code{"tag"} _tag_ +Matches transactions tagged with the given tag. + +@item @code{"tls"} +Specify that transaction should take place in a TLS channel. +@end itemize + +@end itemize + +@item Data Type: opensmtpd-local-delivery +This data type represents the configuration of an +@code{} record. + +@itemize +@item @code{name} (default: @code{#f}) +@code{name} is the string name of the relay action. + +@item @code{method} (default: @code{"mbox"}) +The email delivery option. Valid options are: + +@itemize +@item @code{"mbox"} +Deliver the message to the user's mbox with mail.local(8). + +@item @code{"expand-only"} +Only accept the message if a delivery method was specified in an aliases +or .forward file. + +@item @code{"forward-only"} +Only accept the message if the recipient results in a remote address after +the processing of aliases or forward file. + +@item @code{} +Deliver the message to an LMTP server at @code{}'s +fieldname @code{destination}. The location may be expressed as string +host:port or as a UNIX socket. Optionally, @code{}'s +fieldname @code{rcpt-to} might be specified to use the recipient email +address (after expansion) instead of the local user in the LMTP session +as RCPT TO. + +@item @code{} +Deliver the message to the maildir in +@code{}'s fieldname @code{pathname} if specified, +or by default to @code{"~/Maildir"}. + +The pathname may contain format specifiers that are expanded before use +(see the below section about Format Specifiers). + +If @code{}'s record fieldname @code{junk} is @code{#t}, +then message will be moved to the ‘Junk’ folder if it contains a positive +‘X-Spam’ header. This folder will be created under fieldname @code{pathname} if +it does not yet exist. + +@item @code{} +Delegate the delivery to the @code{}'s fieldname +@code{command} (type string) that receives the message on its standard input. + +The @code{command} may contain format specifiers that are expanded before use +(see Format Specifiers). +@end itemize + +@item @code{alias} (default: @code{#f}) +Use the @code{mapping table} for aliases expansion. + +@item @code{ttl} (default: @code{#f}) +@code{ttl} is a string specify how long a message may remain in the queue. It's +format is @code{n@{s|m|h|d@}}. eg: @code{"4m"} is four minutes. + +@item @code{user} (default: @code{#f} ) +@code{user} is the string username for performing the delivery, to be looked up +with getpwnam(3). + +This is used for virtual hosting where a single username is in charge of +handling delivery for all virtual users. + +This option is not usable with the mbox delivery method. + +@item @code{userbase} (default: @code{#f}) +@code{userbase} is an @code{} record for mapping user +lookups instead of the getpwnam(3) function. + +The fieldnames @code{user} and @code{userbase} are mutually exclusive. + +@item @code{virtual} (default: @code{#f}) +@code{virtual} is an @code{} record is used for virtual +expansion. +@end itemize + +@item Data Type: opensmtpd-relay +This data type represents the configuration of an +@code{} record. + +@itemize +@item @code{name} (default: @code{#f}) +@code{name} is the string name of the relay action. + +@item @code{backup} (default: @code{#f}) +When @code{#t}, operate as a backup mail exchanger delivering messages to any +mail exchanger with higher priority. + +@item @code{backup-mx} (default: @code{#f}) +Operate as a backup mail exchanger delivering messages to any mail exchanger +with higher priority than mail exchanger identified as string name. + +@item @code{helo} (default: @code{#f}) +Advertise string heloname as the hostname to other mail exchangers during +the HELO phase. + +@item @code{helo-src} (default: @code{#f} ) + Use the mapping @code{} to look up a hostname +matching the source address, to advertise during the HELO phase. + +@item @code{domain} (default: @code{#f}) +Do not perform MX lookups but look up destination domain in an +@code{} and use matching relay url as relay host. + +@item @code{host} (default: @code{#f}) +Do not perform MX lookups but relay messages to the relay host described by +the string relay-url. The format for relay-url is +@samp{[proto://[label@@]]host[:port]}. The following protocols are available: + +@multitable {aaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @code{smtp} +@tab Normal SMTP session with opportunistic STARTTLS (the default). +@item @code{smtp+tls} +@tab Normal SMTP session with mandatory STARTTLS@. +@item @code{smtp+notls} +@tab Plain text SMTP session without TLS@. +@item @code{lmtp} +@tab LMTP session. port is required. +@item @code{smtps} +@tab SMTP session with forced TLS on connection, default port is +@item +@tab 465. +@end multitable + +Unless noted, port defaults to 25. + +The label corresponds to an entry in a credentials table, as documented in +@code{man table}. It is used with the @code{"smtp+tls"} and @code{"smtps"} protocols for +authentication. Server certificates for those protocols are verified by +default. + +@item @code{pki} (default: @code{#f}) +For secure connections, use the certificate associated with +@code{} (declared in a pki directive) to prove the +client's identity to the remote mail server. + +@item @code{srs} (default: @code{#f}) +If @code{#t}, then when relaying a mail resulting from a forward, use the Sender +Rewriting Scheme to rewrite sender address. + +@item @code{tls} (default: @code{#f}) boolean or string ``no-verify'' +When @code{#t}, Require TLS to be used when relaying, using mandatory STARTTLS by +default. When used with a smarthost, the protocol must not be +@samp{"smtp+notls://"}. When string @code{"no-verify"}, then do not require a valid +certificate. + +@item @code{auth} (default: @code{#f}) @code{} +Use the alist @code{} for connecting to relay-url +using credentials. This option is usable only with fieldname @code{host} option. + +@item @code{mail-from} (default: @code{#f}) string +Use the string mailaddress as MAIL FROM address within the SMTP transaction. + +@item @code{src} (default: @code{#f}) string | @code{} +Use the string or @code{} sourceaddr for the +source IP address, which is useful on machines with multiple interfaces. If +the list contains more than one address, all of them are used in such a way +that traffic is routed as efficiently as possible. +@end itemize + +@item Data Type: opensmtpd-filter +This data type represents the configuration of an +@code{}. This is the filter record one should use +if they want to use an external package to filter email eg: rspamd or +spamassassin. + +@itemize +@item @code{name} (default: @code{#f}) +The string name of the filter. + +@item @code{proc} (default: @code{#f}) +The string command or process name. If @code{proc-exec} is @code{#t}, @code{proc} is +treated as a command to execute. Otherwise, it is a process name. + +@item @code{proc-exec} (default: @code{#f}) +If @code{#t}, then execute the command in @code{proc}. +@end itemize + +@item Data Type: opensmtpd-filter-phase +This data type represents the configuration of an +@code{}. + +In a regular workflow, @code{smtpd(8)} may accept or reject a message +based only on the content of envelopes. Its decisions are about the +handling of the message, not about the handling of an active session. + +Filtering extends the decision making process by allowing +@code{smtpd(8)} to stop at each phase of an SMTP session, check that +options are met, then decide if a session is allowed to move forward. + +With filtering via an @code{} record, a +session may be interrupted at any phase before an envelope is complete. A +message may also be rejected after being submitted, regardless of whether the +envelope was accepted or not. + +@itemize +@item @code{name} (default: @code{#f}) + +The string name of the filter phase. + +@item @code{phase-name} (default: @code{#f}) +The string name of the phase. Valid values are: + +@multitable {aaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @code{"connect"} +@tab upon connection, before a banner is displayed +@item @code{"helo"} +@tab after HELO command is submitted +@item @code{"ehlo"} +@tab after EHLO command is submitted +@item @code{"mail-from"} +@tab after MAIL FROM command is submitted +@item @code{"rcpt-to"} +@tab after RCPT TO command is submitted +@item @code{"data"} +@tab after DATA command is submitted +@item @code{"commit"} +@tab after message is fully is submitted +@end multitable + +@item @code{options} (default @code{#f}) +A list of unique @code{} records. + +At each phase, various options, specified by a list of +@code{}, may be checked. The +@code{}'s fieldname @code{option} values of: +@code{"fcrdns"}, @code{"rdns"}, and @code{"src"} data are available in +all phases, but other data must have been already submitted before they +are available. Options with a @code{
} next to them require the +@code{}'s fieldname @code{data} to be an +@code{}. There are the available options: + +@multitable {aaaaaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @code{"fcrdns"} +@tab forward-confirmed reverse DNS is valid +@item @code{"rdns"} +@tab session has a reverse DNS +@item @code{"rdns"}
+@tab session has a reverse DNS in table +@item @code{"src"}
+@tab source address is in table +@item @code{"helo"}
+@tab helo name is in table +@item @code{"auth"} +@tab session is authenticated +@item @code{"auth"}
+@tab session username is in table +@item @code{"mail-from"}
+@tab sender address is in table +@item @code{"rcpt-to"}
+@tab recipient address is in table +@end multitable + +These conditions may all be negated by setting +@code{(opensmtpd-option (bool #f))}. + +Any conditions that require a table may indicate that tables include regexs +setting @code{(opensmtpd-option (regex #t))}. + +@item @code{decision} +A string decision to be taken. Some decisions require an @code{message} +or @code{value}. The value and message may be put in the +@code{}'s fieldname @code{data}. Valid strings are: + +@multitable {aaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @code{"bypass"} +@tab the session or transaction bypasses filters +@item @code{"disconnect"} message +@tab the session is disconnected with message +@item @code{"junk"} +@tab the session or transaction is junked, +@item +@tab i.e., an ‘X-Spam: yes’ header is added to +@item +@tab any messages +@item @code{"reject"} message +@tab the command is rejected with message +@item @code{"rewrite"} value +@tab the command parameter is rewritten with value +@end multitable + +Decisions that involve a message require that the message be RFC valid, +meaning that they should either start with a 4xx or 5xx status code. +Descisions can be taken at any phase, though junking can only happen before +a message is committed. + +@item @code{message} (default @code{#f}) +A string message beginning with a 4xx or 5xx status code. + +@item @code{value} (default: @code{#f}) +A number value. @code{value} and @code{message} are mutually exclusive. +@end itemize + +@item Data Type: opensmtpd-option +This data type represents the configuration of an +@code{}, which is used by +@code{} and @code{} +to match various options for email. + +@itemize +@item @code{option} (default @code{#f}) string + +A string option to be taken. Some options require the fieldname +@code{data} to have a string or an @code{}. When the +option record is used inside of an @code{}, then +valid strings for fieldname @code{option} are: + +@itemize +@item @code {"fcrdns"} +@item @code {"rdns"} +@item @code {"src"} +@item @code {"helo"} +@item @code {"auth"} +@item @code {"mail-from"} +@item @code {"rcpt-to"} +@end itemize + +When @code{} is used inside of an +@code{}, then valid strings for fieldname @code{option} +are: + +@itemize +@item @code {"for"} +@item @code {"for any"} +@item @code {"for local"} +@item @code {"for domain"} +@item @code {"for rcpt-to"} +@item @code {"from any"} +@item @code {"from auth"} +@item @code {"from local"} +@item @code {"from mail-from"} +@item @code {"from rdns"} +@item @code {"from socket"} +@item @code {"from src"} +@item @code {"auth"} +@item @code {"helo"} +@item @code {"mail-from"} +@item @code {"rcpt-to"} +@item @code {"tag"} +@item @code {"tls"} +@end itemize + +@item @code{data} (default @code{#f}) string | @code{} +Some options require a string or @code{} to be +present. One would specify that table here. + +@item @code{regex} (default: @code{#f}) boolean +Any options using a table may indicate that tables hold regular +expressions by setting this option to @code{#t}. + +@item @code{bool} (default: @code{#t}) boolean +When @code{(bool #f)}, this option record is negated. +@end itemize + +@item Data Type: opensmtpd-table +This data type represents the configuration of an +@code{}. + +@itemize +@item @code{name} (default @code{#f}) +@code{name} is the name of the @code{} record. + +@item @code{data} (default: @code{#f}) string | list | alist | nested-list +@code{data} expects a string, a list of strings, an alist of strings, or +a nested list of strings. +eg: + +@itemize + +@item string +@lisp +(data "dev@@gnu.org") +@end lisp + +A table of this type is called a @code{string table}. + +@item list +@lisp +(data (list ("gnu.org" "fsf.org"))) +@end lisp + +A table of this type is called a @code{list table}. + +@item alist +@lisp +(data '(("james" . "$encryptedPassword") + ("jennifer" . "$encryptedPassword2))) +@end lisp + +A table of this type is called a @code{mapping table}. + +@item nested-list +@lisp +(data '(("user1" "root@@gnu.org" "admin@@gnu.org") + ("user2" "james@@guix.gnu.org" "sarah@@fsf.org"))) +@end lisp + +A table of this type is also called a @code{mapping table}. + +@end itemize +@end itemize + +@item Data Type: opensmtpd-pki +This data type represents the configuration of an +@code{}. + +@itemize +@item @code{domain} (default @code{#f}) +@code{domain} is the string name of the @code{} record. + +@item @code{cert} (default: @code{#f}) +@code{cert} (default: @code{#f}) + +@code{cert} is the string certificate filename to use for this pki. + +@item @code{key} (default: @code{#f}) +@code{key} is the string certificate falename to use for this pki. + +@item @code{dhe} (default: @code{"none"}) +Specify the DHE string parameter to use for DHE cipher suites with host +pkiname. Valid parameter values are @code{"none"}, @code{"legacy"}, or +@code{"auto"}. For @code{"legacy"}, a fixed key length of 1024 bits is +used, whereas for @code{"auto"}, the key length is determined +automatically. The default is @code{"none"}, which disables DHE cipher +suites. +@end itemize + +@item Data Type: opensmtpd-maildir +@itemize +@item @code{pathname} (default: @code{"~/Maildir"}) +Deliver the message to the maildir if pathname if specified, or by default +to @code{"~/Maildir"}. + +The pathname may contain format specifiers that are expanded before use +(see FORMAT SPECIFIERS). + +@item @code{junk} (default: @code{#f}) +If the junk argument is @code{#t}, then the message will be moved to the @samp{‘Junk’} +folder if it contains a positive @samp{‘X-Spam’} header. This folder will be +created under pathname if it does not yet exist. +@end itemize + +@item Data Type: opensmtpd-mda +This record lets you delegate the delivery to a command that receives +the message on its standard input. + +@itemize +@item @code{name} +The string name for this MDA command. + +@item @code{command} +The command to that delivers the mail. + +The command may contain format specifiers that are expanded before use (see +FORMAT SPECIFIERS). +@end itemize + +@item Data Type: opensmtpd-queue +@itemize +@item @code{compression} (default @code{#f}) +Store queue files in a compressed format. This may be useful to save disk +space. + +@item @code{encryption} (default @code{#f}) +Encrypt queue files with EVP@math{_aes}@math{_256}@math{_gcm}(3). If no key is specified, it is +read with getpass(3). If the string stdin or a single dash (‘-’) is given +instead of a key, the key is read from the standard input. + +@item @code{ttl-delay} (default @code{#f}) +Set the default expiration time for temporarily undeliverable messages, +given as a positive decimal integer followed by a unit s, m, h, or d. The +default is four days (@code{"4d"}). +@end itemize + +@item Data Type: opensmtpd-smtp +Data type representing an @code{} record. + +@itemize +@item @code{ciphers} (default: @code{#f}) +Set the control string for SSL@math{_CTX}@math{_set}@math{_cipher}@math{_list}(3). The default is + ``HIGH:!aNULL:!MD5''. + +@item @code{limit-max-mails} (default: @code{100}) +Limit the number of messages to count for each sessio + +@item @code{limit-max-rcpt} (default: @code{1000}) +Limit the number of recipients to count for each transaction. + +@item @code{max-message-size} (default: @code{35M}) +Reject messages larger than size, given as a positive number of bytes or as +a string to be parsed with scan@math{_scaled}(3). + +@item @code{sub-addr-delim character} (default: @code{+}) +When resolving the local part of a local email address, ignore the ASCII +character and all characters following it. This is helpful for email +filters. @samp{"admin+bills@@gnu.org"} is the same email address as +@samp{"admin@@gnu.org"}. BUT an email filter can filter emails addressed to first +email address into a 'Bills' email folder. +@end itemize + +@item Data Type: opensmtpd-srs +Use this record to set up the Sender Rewriting Scheme (SRS). + +@itemize +@item @code{key} (default: @code{#f}) +Set the secret key to use for SRS, the Sender Rewriting Scheme. + +@item @code{backup-key} (default: @code{#f}) +Set a backup secret key to use as a fallback for SRS@. This can be used to +implement SRS key rotation. + +@item @code{ttl-delay} (default: @code{"4d"}) +Set the time-to-live delay for SRS envelopes. After this delay, a bounce +reply to the SRS address will be discarded to limit risks of forged +addresses. +@end itemize + +@item Format Specifiers +Some configuration records support expansion of their parameters at +runtime. Such records (for example +@code{}, @code{}) may use +format specifiers which are expanded before delivery or relaying. The +following formats are currently supported: + +@multitable {aaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @samp{%@{sender@}} +@tab sender email address, may be empty string +@item @samp{%@{sender.user@}} +@tab user part of the sender email address, may be empty +@item @samp{%@{sender.domain@}} +@tab domain part of the sender email address, may be empty +@item @samp{%@{rcpt@}} +@tab recipient email address +@item @samp{%@{rcpt.user@}} +@tab user part of the recipient email address +@item @samp{%@{rcpt.domain@}} +@tab domain part of the recipient email address +@item @samp{%@{dest@}} +@tab recipient email address after expansion +@item @samp{%@{dest.user@}} +@tab user part after expansion +@item @samp{%@{dest.domain@}} +@tab domain part after expansion +@item @samp{%@{user.username@}} +@tab local user +@item @samp{%@{user.directory@}} +@tab home directory of the local user +@item @samp{%@{mbox.from@}} +@tab name used in mbox From separator lines +@item @samp{%@{mda@}} +@tab mda command, only available for mda wrappers +@end multitable + +Expansion formats also support partial expansion using the optional bracket notations +with substring offset. For example, with recipient domain @samp{“example.org”}: + +@multitable {aaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaa} +@item @samp{%@{rcpt.domain[0]@}} +@tab expands to “e” +@item @samp{%@{rcpt.domain[1]@}} +@tab expands to “x” +@item @samp{%@{rcpt.domain[8:]@}} +@tab expands to “org” +@item @samp{%@{rcpt.domain[-3:]@}} +@tab expands to “org” +@item @samp{%@{rcpt.domain[0:6]@}} +@tab expands to “example” +@item @samp{%@{rcpt.domain[0:-4]@}} +@tab expands to “example” +@end multitable + +In addition, modifiers may be applied to the token. For example, with recipient +@samp{“User+Tag@@Example.org”}: + +@multitable {aaaaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @samp{%@{rcpt:lowercase@}} +@tab expands to “user+tag@@example.org” +@item @samp{%@{rcpt:uppercase@}} +@tab expands to “USER+TAG@@EXAMPLE.ORG” +@item @samp{%@{rcpt:strip@}} +@tab expands to “User@@Example.org” +@item @samp{%@{rcpt:lowercasestrip@}} +@tab expands to “user@@example.org” +@end multitable + +For security concerns, expanded values are sanitized and potentially dangerous +characters are replaced with ‘:’. In situations where they are desirable, the +“raw” modifier may be applied. For example, with recipient +@samp{“user+t?g@@example.org”}: + +@multitable {aaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @samp{%@{rcpt@}} +@tab expands to “user+t:g@@example.org” +@item @samp{%@{rcpt:raw@}} +@tab expands to “user+t?g@@example.org” +@end multitable +@end itemize + @subsubheading Exim Service @cindex mail transfer agent (MTA) diff --git a/gnu/services/mail.scm b/gnu/services/mail.scm index 43f144a42d..4175cab375 100644 --- a/gnu/services/mail.scm +++ b/gnu/services/mail.scm @@ -35,6 +35,10 @@ (define-module (gnu services mail) #:use-module (gnu packages admin) #:use-module (gnu packages dav) #:use-module (gnu packages tls) + #:use-module (guix i18n) + #:use-module (guix diagnostics) + #:use-module (guix ui) + #:use-module (guix utils) #:use-module (guix records) #:use-module (guix packages) #:use-module (guix gexp) @@ -58,10 +62,149 @@ (define-module (gnu services mail) mailbox-configuration namespace-configuration + opensmtpd-table + opensmtpd-table? + opensmtpd-table-name + opensmtpd-table-data + + opensmtpd-ca + opensmtpd-ca? + opensmtpd-ca-name + opensmtpd-ca-file + + opensmtpd-pki + opensmtpd-pki? + opensmtpd-pki-domain + opensmtpd-pki-cert + opensmtpd-pki-key + opensmtpd-pki-dhe + + opensmtpd-local-delivery + opensmtpd-local-delivery? + opensmtpd-local-delivery-method + opensmtpd-local-delivery-alias + opensmtpd-local-delivery-ttl + opensmtpd-local-delivery-user + opensmtpd-local-delivery-userbase + opensmtpd-local-delivery-virtual + opensmtpd-local-delivery-wrapper + + opensmtpd-maildir + opensmtpd-maildir? + opensmtpd-maildir-pathname + opensmtpd-maildir-junk + + opensmtpd-mda + opensmtpd-mda-name + opensmtpd-mda-command + + opensmtpd-lmtp + opensmtpd-lmtp-destination + opensmtpd-lmtp-rcpt + + opensmtpd-relay + opensmtpd-relay? + opensmtpd-relay-name + opensmtpd-relay-backup + opensmtpd-relay-backup-mx + opensmtpd-relay-helo + opensmtpd-relay-domain + opensmtpd-relay-host + opensmtpd-relay-pki + opensmtpd-relay-srs + opensmtpd-relay-tls + opensmtpd-relay-auth + opensmtpd-relay-mail-from + opensmtpd-relay-src + + opensmtpd-option + opensmtpd-option? + opensmtpd-option-option + opensmtpd-option-bool + opensmtpd-option-regex + opensmtpd-option-data + + opensmtpd-filter-phase + opensmtpd-filter-phase? + opensmtpd-filter-phase-name + opensmtpd-filter-phase-phase + opensmtpd-filter-phase-options + opensmtpd-filter-phase-decision + opensmtpd-filter-phase-message + opensmtpd-filter-phase-value + + opensmtpd-filter + opensmtpd-filter? + opensmtpd-filter-name + opensmtpd-filter-proc + + opensmtpd-interface + opensmtpd-interface? + opensmtpd-interface-interface + opensmtpd-interface-family + opensmtpd-interface-auth + opensmtpd-interface-auth-optional + opensmtpd-interface-filters + opensmtpd-interface-hostname + opensmtpd-interface-hostnames + opensmtpd-interface-mask-src + opensmtpd-interface-disable-dsn + opensmtpd-interface-pki + opensmtpd-interface-port + opensmtpd-interface-proxy-v2 + opensmtpd-interface-received-auth + opensmtpd-interface-senders + opensmtpd-interface-masquerade + opensmtpd-interface-secure-connection + opensmtpd-interface-tag + + opensmtpd-socket + opensmtpd-socket? + opensmtpd-socket-filters + opensmtpd-socket-mask-src + opensmtpd-socket-tag + + opensmtpd-match + opensmtpd-match? + opensmtpd-match-action + opensmtpd-match-options + + opensmtpd-smtp + opensmtpd-smtp? + opensmtpd-smtp-ciphers + opensmtpd-smtp-limit-max-mails + opensmtpd-smtp-limit-max-rcpt + opensmtpd-smtp-max-message-size + opensmtpd-smtp-sub-addr-delim character + + opensmtpd-srs + opensmtpd-srs? + opensmtpd-srs-key + opensmtpd-srs-backup-key + opensmtpd-srs-ttl-delay + + opensmtpd-queue + opensmtpd-queue? + opensmtpd-queue-compression + opensmtpd-queue-encryption + opensmtpd-queue-ttl-delay + opensmtpd-configuration opensmtpd-configuration? + opensmtpd-package + opensmtpd-config-file + opensmtpd-configuration-bounce + opensmtpd-configuration-cas + opensmtpd-configuration-interfaces + opensmtpd-configuration-socket + opensmtpd-configuration-includes + opensmtpd-configuration-matches + ;;opensmtpd-configuration-mda-wrappers + opensmtpd-configuration-mta-max-deferred + opensmtpd-configuration-srs + opensmtpd-configuration-smtp + opensmtpd-configuration-queue opensmtpd-service-type - %default-opensmtpd-config-file mail-aliases-service-type @@ -1641,22 +1784,2351 @@ (define (generate-dovecot-documentation) (listeners unix-listener-configuration fifo-listener-configuration inet-listener-configuration)) (protocol-configuration ,protocol-configuration-fields)) - 'dovecot-configuration)) + 'dovecot-configuration)) -;;; ;;; OpenSMTPD. ;;; +;;; This next bit of code helps me create my own sanitizer functions. + +;; some fieldnames have a default value of #f, which is ok. They cannot have +;; a value of #t. +;; for example opensmtpd-table-data can be #f, BUT NOT true. +;; my/sanitize procedure tests values to see if they are of the right kind. +;; procedure false? is needed to allow fields like 'values' to be blank, +;; (empty), or #f BUT also have a value like a list of strings. +(define (false? var) + (eq? #f var)) + +;; TODO I have to have this procedure, or I need to change my/sanitize +;; procedure. +(define (my-file-exists? file) + (and (string? file) + (access? file F_OK))) + +;; This procedure takes in a var and a list of procedures. It loops through +;; list of procedures passing in var to each. +;; if one procedure returns #t, the function returns true. Otherwise #f. +;; TODO for fun rewrite this using map +;; If I rewrote it in map, then it may help with sanitizing. +;; eg: I could then potentially easily sanitize vars with lambda procedures. +(define (is-value-right-type? var list-of-procedures record fieldname) + (if (null? list-of-procedures) + #f + (if ((car list-of-procedures) var) + #t + (is-value-right-type? var (cdr list-of-procedures) record + fieldname)))) + +;; converts strings like this: +;; "apple, ham, cherry" -> "apple, ham, or cherry" +;; "pineapple" -> "pinneapple". +;; "cheese, grapefruit, or jam" -> "cheese, grapefruit, or jam" +(define (add-comma-or string) + (define last-comma-location (string-rindex string #\,)) + (if last-comma-location + (if (string-contains string ", or" last-comma-location) + string + (string-replace string ", or" last-comma-location + (+ 1 last-comma-location))) + string)) + + +(define (list-of-procedures->string procedures) + (define string + (let loop ((procedures procedures)) + (if (null? procedures) + "" + (begin + (string-append + (cond ((eq? false? (car procedures)) + "#f, ") + ((eq? boolean? (car procedures)) + "a boolean, ") + ((eq? string? (car procedures)) + "a string, ") + ((eq? integer? (car procedures)) + "an integer, ") + ((eq? list-of-strings? (car procedures)) + "a list of strings, ") + ((eq? assoc-list? (car procedures)) + "an association list of strings, ") + ((eq? nested-list? (car procedures)) + "a nested-list of strings, ") + ((eq? opensmtpd-pki? (car procedures)) + "an record, ") + ((eq? opensmtpd-table? (car procedures)) + "an record, ") + ((eq? list-of-opensmtpd-match? (car procedures)) + "a list of unique records, ") + ((eq? list-of-strings-or-gexps? (car procedures)) + "a list of strings or gexps, ") + ;; TODO can I remove the next two procedures? + ;; tables-data-are-a* ? I think I can. + ((eq? tables-data-are-assoc-list? (car procedures)) + (string-append + "an record whose fieldname 'data' are " + "an assoc-list.\nFor example: (opensmtpd-table " + "(name \"hostnames\") , " + "(data '((\"124.394.23.1\" . \"gnu.org\"))))")) + ((eq? tables-data-are-a-list-of-strings? + (car procedures)) + (string-append + "on record whose fieldname 'data' is " + "a list of strings.\n" + "For example: (opensmtpd-table (name \"domains\") , " + "(data (list \"gnu.org\" \"guix.gnu.org\")))")) + ((eq? my-file-exists? (car procedures)) + "a file, ") + (else "has an incorrect value, ")) + (loop (cdr procedures))))))) + (add-comma-or (string-append (string-drop-right string 2) ".\n"))) + +(define (list-of-strings-or-gexps? list) + (and (list? list) + (cond ((null? list) + #t) + ((or (string? (car list)) + (gexp? (car list)) + (local-file? (car list)) + (file-append? (car list)) + (plain-file? (car list)) + (computed-file? (car list)) + (program-file? (car list))) + (list-of-strings-or-gexps? (cdr list))) + (else #f)))) + +(define (my/sanitize var record fieldname list-of-procedures) + (define try-string + (string-append "Try " (list-of-procedures->string list-of-procedures))) + (if (is-value-right-type? var list-of-procedures record fieldname) + var + (begin + (cond ((string? var) + (report-error (G_ "(~a \"~a\") is invalid.~%") fieldname var)) + ((or (number? var) (boolean? var)) + (report-error (G_ "(~a ~a) is invalid.~%") fieldname var) ) + (else + (report-error (G_ "(~a ...) is invalid.~%Value is: ~a~%") + fieldname var))) + (display-hint (G_ try-string)) + (throw 'bad! var)))) + +;;; The Opensmtpd records. + +;; Some example opensmtpd-tables: +;; +;; (opensmtpd-table (name "root accounts") +;; (data '(("joshua" . "root@dismail.de") +;; ("joshua" . "postmaster@dismail.de")))) +;; (opensmtpd-table (name "root accounts") +;; (data (list "mysite.me" "your-site.com"))) +;; TODO: How am I supporting file: or db: tables? +;; Perhaps I should just automatically convert the table to a database table +;; if the data gets large enough. What would be sufficently large enough? +(define-record-type* + opensmtpd-table make-opensmtpd-table + opensmtpd-table? + (name opensmtpd-table-name ;; string + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-table" "name" + (list string?))))) + ;; FIXME Support an aliasing table as described here: + ;; https://man.openbsd.org/table.5 + ;; One may have to use the record file for this. I don't think tables + ;; support a table like this: + ;; table "name" { joshua = joshua@gnucode.me,joshua@gnu-hurd.com, \ + ;; joshua@propernaming.org, root = root@gnucode.me } + ;; If values is an absolute filename, then it will use said filename to + ;; house the table info. filename must be an absolute filename. + (data opensmtpd-table-data + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-table" "data" + (list list-of-strings? assoc-list? + nested-list?)))))) + +(define-record-type* + opensmtpd-ca make-opensmtpd-ca + opensmtpd-ca? + (name opensmtpd-ca-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-ca" "name" (list string?))))) + (file opensmtpd-ca-file + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-ca" "file" + (list my-file-exists?)))))) + +(define-record-type* + opensmtpd-pki make-opensmtpd-pki + opensmtpd-pki? + (domain opensmtpd-pki-domain + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-pki" "domain" + (list string?))))) + ;; TODO/FIXME this should probably be a list of files. The opensmtpd + ;; documentation says that you could have a list of files: + ;; + ;; pki pkiname cert certfile + ;; Associate certificate file certfile with host pkiname, and use that file + ;; to prove the identity of the mail server to clients. pkiname is the + ;; server's name, derived from the default hostname or set using either + ;; /gnu/store/2d13sdz76ldq8zgwv4wif0zx7hkr3mh2-opensmtpd-6.8.0p2/etc/mailname + ;; or using the hostname directive. If a fallback certificate or SNI is + ;; wanted, the ‘*’ wildcard may be used as pkiname. + + ;; A certificate chain may be created by appending one or many certificates, + ;; including a Certificate Authority certificate, to certfile. The creation + ;; of certificates is documented in starttls(8). + (cert opensmtpd-pki-cert + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-pki" "cert" + (list my-file-exists?))))) + (key opensmtpd-pki-key + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-pki" "key" + (list my-file-exists?))))) + ; todo sanitize this. valid parameters are "none", "legacy", or "auto". + (dhe opensmtpd-pki-dhe + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-dhe" "dhe" + (list false? string?)))))) + +(define-record-type* + opensmtpd-lmtp make-opensmtpd-lmtp + opensmtpd-lmtp? + (destination opensmtpd-lmtp-destination + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-lmtp" "destination" + (list string?))))) + (rcpt-to opensmtpd-lmtp-rcpt-to + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-lmtp" "rcpt-to" + (list false? string?)))))) + +(define-record-type* + opensmtpd-mda make-opensmtpd-mda + opensmtpd-mda? + (name opensmtpd-mda-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-mda" "name" + (list string?))))) + ;; TODO should I allow this command to be a gexp? + (command opensmtpd-mda-command + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-mda" "command" + (list string?)))))) + +(define-record-type* + opensmtpd-maildir make-opensmtpd-maildir + opensmtpd-maildir? + (pathname opensmtpd-maildir-pathname + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-maildir" "pathname" + (list false? string?))))) + (junk opensmtpd-maildir-junk + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-maildir" "junk" + (list boolean?)))))) + +(define-record-type* + opensmtpd-local-delivery make-opensmtpd-local-delivery + opensmtpd-local-delivery? + (name opensmtpd-local-delivery-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "name" + (list string?))))) + (method opensmtpd-local-delivery-method + (default "mbox") + (sanitize (lambda (var) + (define fieldname "method") + (define options (list "mbox" "expand-only" + "forward-only")) + (define options-plus-records + (append options (list "(opensmtpd-lmtp ...)" + "(opensmtpd-maildir ...)" + "(opensmtpd-mda ...)"))) + (cond ((or (opensmtpd-lmtp? var) + (opensmtpd-maildir? var) + (opensmtpd-mda? var) + (member var options)) + var) + (else + (begin + (report-error (G_ "(~a \"~a\") is invalid.~%") + fieldname var) + (display-hint + (G_ (hint-string + var + options-plus-records + #:fieldname fieldname))) + (throw 'bad! var))))))) + (alias opensmtpd-local-delivery-alias + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "alias" + (list false? + tables-data-are-assoc-list?))))) + (ttl opensmtpd-local-delivery-ttl + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "ttl" + (list false? string?))))) + (user opensmtpd-local-delivery-user + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "user" + (list false? string?))))) + (userbase opensmtpd-local-delivery-userbase + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "userbase" + (list false? + tables-data-are-assoc-list?))))) + (virtual opensmtpd-local-delivery-virtual + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "virtual" + (list + false? + tables-data-are-assoc-list?))))) + (wrapper opensmtpd-local-delivery-wrapper + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "wrapper" + (list false? string?)))))) + +(define-record-type* + opensmtpd-relay make-opensmtpd-relay + opensmtpd-relay? + (name opensmtpd-relay-name + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "name" + (list string?)))) + (default #f)) + (backup opensmtpd-relay-backup ;; boolean + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "backup" + (list boolean?))))) + (backup-mx opensmtpd-relay-backup-mx ;; string mx name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "backup-mx" + (list false? string?))))) + (helo opensmtpd-relay-helo + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "helo" + (list false? string?)))) + (default #f)) + (helo-src opensmtpd-relay-helo-src + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "helo-src" + (list false? string? + tables-data-are-assoc-list?)))) + (default #f)) + (domain opensmtpd-relay-domain + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "domain" + (list false? opensmtpd-table?)))) + (default #f)) + (host opensmtpd-relay-host + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "host" + (list false? string?)))) + (default #f)) + (pki opensmtpd-relay-pki + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "pki" + (list false? opensmtpd-pki?))))) + (srs opensmtpd-relay-srs + (default #f) + (lambda (var) + (my/sanitize var "opensmtpd-relay" "srs" + (list boolean?)))) + (tls opensmtpd-relay-tls + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "tls" + (list false? string?))))) + ;; the table here looks like: + ;; label1 user:password + ;; label2 user2:password2 + ;; It is documented in the credentials table in man table + (auth opensmtpd-relay-auth + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "auth" + (list false? + tables-data-are-assoc-list?)))) + (default #f)) + (mail-from opensmtpd-relay-mail-from + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "mail-from" + (list false? string?)))) + (default #f)) + ;; string "127.0.0.1" or "" or "
" + ;; TODO should I do some sanitizing to make sure that the string? + ;; here is actually an IP address or a valid interface? + (src opensmtpd-relay-src + (sanitize + (lambda (var) + (my/sanitize var "opensmtpd-relay" "src" + (list false? string? + tables-data-are-a-list-of-strings?)))) + (default #f))) + +;; this record is used by & +;; +(define-record-type* + opensmtpd-option make-opensmtpd-option + opensmtpd-option? + (option opensmtpd-option-option + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-option" "option" + (list string?))))) + (bool opensmtpd-option-bool + (default #t) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-option" "not" + (list boolean?))))) + (regex opensmtpd-option-regex + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-option" "regex" + (list boolean?))))) + (data opensmtpd-option-data + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-option" "data" + (list false? string? opensmtpd-table?)))))) + +(define-record-type* + opensmtpd-filter-phase make-opensmtpd-filter-phase + opensmtpd-filter-phase? + (name opensmtpd-filter-phase-name ;; string + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter-phase" "name" + (list string?))))) + (phase opensmtpd-filter-phase-phase ;; string + (default #f) + (sanitize + (lambda (var) + (define options + (list "connect" "helo" "ehlo" "mail-from" + "rcpt-to" "data" "commit")) + (define fieldname "phase") + (if (and (string? var) + (member var options)) + var + (begin + (report-error + (G_ + "(opensmtpd-filter-phase ... (~a \"~a\")) is invalid.~%") + fieldname var) + (display-hint + (G_ (hint-string var options + #:fieldname fieldname))) + (throw 'bad! var)))))) + (options opensmtpd-filter-phase-options + (default #f) + (sanitize + (lambda (var) + (cond + ((false? var) + (report-error (G_ "")) + (display "(opensmtpd-filter-phase (options #f)) is invalid.\n") + (display-hint + (G_ "Try a list of (opensmtpd-option) records.\n")) + (throw 'bad! #f)) + ((not (list-of-opensmtpd-option? var)) + (report-error (G_ "")) + (display "(opensmtpd-filter-phase (options ...) is invalid.\n") + (display-hint + (G_ "Try a list of (opensmtpd-option) records.\n")) + (throw 'bad! var)) + (else (sanitize-options-for-filter-phase var)))))) + (decision opensmtpd-filter-phase-decision + (default #f) + (sanitize + (lambda (var) + (define options + (list "bypass" "disconnect" + "reject" "rewrite" "junk")) + (define fieldname "decision") + (if (and (string? var) + (member var options)) + var + (begin + (report-error (G_ "(~a \"~a\") is invalid.~%") + fieldname var) + (display-hint (G_ (hint-string var options + #:fieldname fieldname))) + (throw 'bad! var)))))) + (message opensmtpd-filter-phase-message + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter-phase" "message" + (list false? string?))))) + (value opensmtpd-filter-phase-value + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter-phase" "value" + (list false? number?)))))) + +(define-record-type* + opensmtpd-filter make-opensmtpd-filter + opensmtpd-filter? + (name opensmtpd-filter-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter" "name" + (list string?))))) + (exec opensmtpd-filter-exec + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter" "exec" + (list boolean?))))) + ;; a string like "rspamd" or the command to start it like + ;; "/path/to/rspamd --option=arg --2nd-option=arg2" + ;; OR a list of strings and/or geps. + (proc opensmtpd-filter-proc + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter" "proc" + (list string? + list-of-strings-or-gexps?)))))) + +;; There is another type of filter that opensmtpd supports, which is a +;; filter chain. A filter chain is a list of s +;; and/or s. This lets you apply several filters under +;; one filter name. I could have defined a record type for it, but the +;; record would only have had two fields: name and list-of-filters. +;; Why write that as a record? It makes the user of this service harder. +;; Instead, just define it as a list, and if a user wants an interface +;; to make multiple filters, he just appends to the 'filters' fieldname. +;; +;; returns #t if list is a unique list of or +;; +;; returns # otherwise +(define (opensmtpd-filter-chain? %filters) + (and (list-of-unique-filter-or-filter-phase? %filters) + (< 1 (length %filters)))) + +(define-record-type* + opensmtpd-interface make-opensmtpd-interface + opensmtpd-interface? + ;; interface may be an IP address, interface group, or domain name + (interface opensmtpd-interface-interface + (default "lo") + (sanitize (lambda (var) + (my/sanitize var "interface" "interface" + (list string?))))) + (family opensmtpd-interface-family + (default #f) + (sanitize + (lambda (var) + (define options (list "inet4" "inet6")) + (define fieldname "family") + (cond + ((eq? #f var) ;; var == #f + var) + ((and (string? var) + (member var options)) + var) + (else + (begin + (report-error (G_ "(~a \"~a\") is invalid.~%") fieldname var) + (display-hint (G_ (hint-string var options + #:fieldname fieldname))) + (throw 'bad! var))))))) + (auth opensmtpd-interface-auth + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "auth" + (list boolean? + tables-data-are-assoc-list?))))) + (auth-optional opensmtpd-interface-auth-optional + (default #f) + (sanitize + (lambda (var) + (my/sanitize var "opensmtpd-interface" "auth-optional" + (list boolean? + tables-data-are-assoc-list?))))) + ;; TODO add a ca entry? + ;; string FIXME/TODO sanitize this to support a gexp. That way way the + ;; includes directive can include my hacky scheme code that I use + ;; for opensmtpd-dkimsign. + (filters opensmtpd-interface-filters + (default #f) + (sanitize (lambda (var) + (sanitize-socket-and-interfaces-filters var)))) + (hostname opensmtpd-interface-hostname + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "hostname" + (list false? string?))))) + (hostnames opensmtpd-interface-hostnames + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "hostnames" + (list + false? + tables-data-are-assoc-list?))))) + (mask-src opensmtpd-interface-mask-src + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "mask-src" + (list boolean?))))) + (disable-dsn opensmtpd-interface-disable-dsn + (default #f)) + (pki opensmtpd-interface-pki + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "pki" + (list false? opensmtpd-pki?))))) + (port opensmtpd-interface-port + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "port" + (list false? integer?))))) + (proxy-v2 opensmtpd-interface-proxy-k2 + (default #f)) + (received-auth opensmtpd-interface-received-auth + (default #f)) + (senders opensmtpd-interface-senders + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "senders" + (list false? + tables-data-are-assoc-list?)))) + (default #f)) + (masquerade opensmtpd-interface-masquerade + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "masquerade" + (list boolean?)))) + (default #f)) + (secure-connection opensmtpd-interface-secure-connection + (default #f) + (sanitize + (lambda (var) + (define options + (list "smtps" "tls" "tls-require" + "tls-require-verify")) + (define fieldname "secure-connection") + (cond ((boolean? var) + var) + ((and (string? var) + (member var options)) + var) + (else + (begin + (report-error + (G_ "(~a \"~a\") is invalid.~%") + fieldname var) + (display-hint + (G_ (hint-string var options + #:fieldname fieldname))) + (throw 'bad! var))))))) + (tag opensmtpd-interface-tag + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "tag" + (list false? string?)))) + (default #f))) + +(define-record-type* + opensmtpd-socket make-opensmtpd-socket + opensmtpd-socket? + ;; false or or list of + (filters opensmtpd-socket-filters + (sanitize (lambda (var) + (sanitize-socket-and-interfaces-filters + var + #:socket-or-interface "socket"))) + (default #f)) + (mask-src opensmtpd-socket-mask-src + (default #f) + (my/sanitize var "opensmtpd-interface" "mask-src" + (list false? boolean?))) + (tag opensmtpd-socket-tag + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "tag" + (list false? string?)))) + (default #f))) + + +(define-record-type* + opensmtpd-match make-opensmtpd-match + opensmtpd-match? + ;;TODO? Perhaps I should add in a reject fieldname. If reject + ;;is #t, then the match record will be a reject match record. + ;; (opensmtpd-match (reject #t)) vs. (opensmtpd-match (action 'reject)) + ;; To do this, I will also have to 'reject' mutually exclusive. + ;; AND an match with 'reject' can have no action defined. + (action opensmtpd-match-action + (default #f) + (sanitize + (lambda (var) + (define fieldname "action") + (if (or (opensmtpd-relay? var) + (opensmtpd-local-delivery? var) + (eq? (quote reject) var)) + var + (begin + (report-error (G_ "(~a \"~a\") is invalid.~%") + fieldname var) + (display-hint + (G_ "Try an (opensmtpd-relay) record, +(opensmtpd-local-delivery) record, or (quote reject).")) + (throw 'bad! var)))))) + (options opensmtpd-match-options + (default #f) + (sanitize (lambda (var) + (sanitize-options-for-opensmtpd-match var))))) + +(define-record-type* + opensmtpd-smtp make-opensmtpd-smtp + opensmtpd-smtp? + (ciphers opensmtpd-smtp-ciphers + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp" "ciphers" + (list false? string?))))) + (limit-max-mails opensmtpd-smtp-limit-max-mails + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp" + "limit-max-mails" + (list false? integer?))))) + (limit-max-rcpt opensmtpd-smtp-limit-max-rcpt + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp" + "limit-max-rcpt" + (list false? integer?))))) + ;; TODO the user could enter in "zebra" which would break the config. + ;; I should sanitize the string to make sure it looks like "50M". + (max-message-size opensmtpd-smtp-max-message-size + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp" + "max-message-size" + (list false? integer? + string?))))) + ;; FIXME/TODO the sanitize function of sub-addr-delim should accept a + ;; string of length one not string? + (sub-addr-delim opensmtpd-smtp-sub-addr-delim + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp" + "sub-addr-delim" + (list false? integer? string?)))))) + +(define-record-type* + opensmtpd-srs make-opensmtpd-srs + opensmtpd-srs? + (key opensmtpd-srs-key + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-srs" "key" + (list false? boolean? my-file-exists?))))) + (backup-key opensmtpd-srs-backup-key + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-srs" "backup-key" + (list false? integer? + my-file-exists?))))) + ;; TODO the user could set the string to be "zebra", which would break + ;; the config. + (ttl-delay opensmtpd-srs-ttl-delay + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-srs" "ttl-delay" + (list false? string?)))))) + +(define-record-type* + opensmtpd-queue make-opensmtpd-queue + opensmtpd-queue? + (compression opensmtpd-queue-compression + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-queue" "compression" + (list boolean?))))) + (encryption opensmtpd-queue-encryption + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-queue" "encryption" + (list boolean? string? + my-file-exists?))))) + ;; TODO the user could set the string to be "zebra", which would break + ;; the config. + (ttl-delay opensmtpd-queue-ttl-delay + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-queue" "ttl-delay" + (list false? string?)))))) (define-record-type* opensmtpd-configuration make-opensmtpd-configuration opensmtpd-configuration? - (package opensmtpd-configuration-package - (default opensmtpd)) + (package opensmtpd-configuration-package + (default opensmtpd)) (config-file opensmtpd-configuration-config-file - (default %default-opensmtpd-config-file)) + (default #f)) + ;; FIXME/TODO should I include a admd authservid entry? + (bounce opensmtpd-configuration-bounce + (default #f) + (sanitize + (lambda (var) + (cond ((false? var) + var) + ((and (list? var) + (>= 4 (length var)) + (<= 1 (length var)) + (list-of-strings? var) + (every (lambda (str) + (and (<= 2 (string-length str)) + ;; last character of str is 's' or 'm' + ;; or 'h' or 'd'. + (member (string-take-right str 1) + (list "s" "m" "h" "d")) + ;; first part of str is an integer. + (integer? + (string->number + (string-take str + (- (string-length str) + 1 )))))) + var)) + var) + (else + ;; FIXME TODO I am getting a warning that says + ;; possibly wrong number of arguments to `G_' + ;; is one of the below lines to blame? + (if (string? var) + (report-error (G_ "(bounce \"~a\") is invalid.\n") var) + (report-error (G_ "(bounce ~a) is invalid.\n") var)) + (display-hint (G_ "Try (bounce (list \"30m\" \"2h\"))\n")) + (throw 'bad! var)))))) + (cas opensmtpd-configuration-cas + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "cas" + (list false? list-of-opensmtpd-ca?))))) + + ;; list of many records of type opensmtpd-interface + (interfaces opensmtpd-configuration-interfaces + (default (list (opensmtpd-interface))) + (sanitize + (lambda (var) + ;; This makes sure that no opensmtpd-interface is like this: + ;; (opensmtpd-interface (senders #f) (masquerade #t)), which + ;; is a syntax error. + (define (correct-senders? interface) + (not + (and (not (opensmtpd-interface-senders interface)) + (opensmtpd-interface-masquerade interface)))) + + (define fieldname "interface") + + ;; TODO rework this sanitize bit, so that if someone writes: + ;; (opensmtpd-interface (senders #f) (masquerade #t)), they + ;; get a proper error. + ;; (report-error + ;; (G_ "((senders #f) & (masquerade #t)) is invalid.\n")) + (if (and (list-of-interface? var) + (every correct-senders? var) + (not (contains-duplicate? var))) + var + (begin + (display " fieldname ") + (display "'interface' may be #f or a list of records") + (display "\n of unique .\n") + (throw 'bad! var)))))) + (socket opensmtpd-configuration-socket + (default #f) + (sanitize + (lambda (var) + (define fieldname "socket") + (if (or (opensmtpd-socket? var) + (false? var)) + var + (begin + (report-error (G_ "(~a \"~a\") is invalid.~%") + fieldname var) + (display-hint + (G_ + (string-append "Try an (" + fieldname + " (opensmtpd-socket ...)) .\n"))) + (throw 'bad! var)))))) + ;; list of strings of absolute path names + (includes opensmtpd-configuration-includes + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "includes" + (list false? list-of-strings? gexp?))))) + (matches opensmtpd-configuration-matches + (default (list (opensmtpd-match + (action (opensmtpd-local-delivery + (name "local") + (method "mbox"))) + (options (list + (opensmtpd-option + (option "for local"))))) + (opensmtpd-match + (action (opensmtpd-relay + (name "outbound"))) + (options (list + (opensmtpd-option + (option "from local")) + (opensmtpd-option + (option "for any"))))))) + ;; TODO perhaps I should sanitize this function like I sanitized + ;; the 'filters'. For example, you could have two different. + ;; actions, one for local delivery and one for remote, + ;; with the same name. + ;; It might be a bit complicated to do this. + ;; I might just let smtpd figure out if the user made a silly + ;; mistake by having two different actions with the same name. + (sanitize (lambda (var) + var + (my/sanitize var "opensmtpd-configuration" "matches" + (list list-of-opensmtpd-match?))))) + ;; list of many records of type mda-wrapper + ;; TODO/FIXME support using gexps here + ;; eg (list "name" gexp) + ;; TODO what are mda-wrappers for? How do I use this fieldname? + ;; (mda-wrappers opensmtpd-configuration-mda-wrappers + ;; (default #f) + ;; (sanitize (lambda (var) + ;; (my/sanitize var + ;; "opensmtpd-configuration" + ;; "mda-wrappers" + ;; (list false? string?))))) + (mta-max-deferred opensmtpd-configuration-mta-max-deferred + (default 100) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" + "mta-max-deferred" + (list number?))))) + (queue opensmtpd-configuration-queue + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "queue" + (list false? opensmtpd-queue?))))) + (smtp opensmtpd-configuration-smtp + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "smtp" + (list false? opensmtpd-smtp?))))) + (srs opensmtpd-configuration-srs + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "srs" + (list false? opensmtpd-srs?))))) (setgid-commands? opensmtpd-setgid-commands? (default #t))) +;; this help procedure is used 3 or 4 times by +;; sanitize-options-for-opensmtpd-match +(define* (throw-error-duplicate-option option error-arg + #:key (record-name "match")) + (throw-error error-arg + (list + (string-append "(opensmtpd-" record-name + ") (options ...)) can only have one \n" + "(opensmtpd-option (option \"" option + "\")) record, but more are present. \n")) + #:duplicate-option #t)) + +;; this procedure sanitizes the fieldname opensmtpd-match-options +(define* (sanitize-options-for-opensmtpd-match %options) + (define option-list (list "for any" "for local" "for domain" + "for rcpt-to" "from any" "from auth" + "from local" "from mail-from" "from rdns" + "from socket" "from src" "auth" "helo" + "mail-from" "rcpt-to" "tag" "tls")) + (when (not (list-of-opensmtpd-option? %options)) + (report-error (G_ "")) + (display + (string-append "(opensmtpd-match (options ...)) is a list of unique" + " (opensmtpd-option ...) records.\nIt's value is: ")) + (display %options) + (display "\n") + (throw 'bad! %options)) + + (let loop ((%traversing-options %options) + ;; sanitized-options is an alist that may end of looking + ;; like: + ;; (("for" (opensmtpd-option (option "for any"))) + ;; ("from" (opensmtpd-option (option "from any"))) + ;; ("tag (opensmtpd-option (option "tag") (data "tag"))) + (%sanitized-options '()) + (option-record (if (null? %options) + '() + (car %options))) + (option-string (if (null? %options) + '() + (opensmtpd-option-option + (car %options))))) + + (cond + ((null? %traversing-options) + %options) + ;; error if option-string is invalid option + ((not (member option-string option-list)) + (report-error (G_ "(opensmtpd-match \"~a\")) is invalid.\n") + option-string) + (display-hint (G_ "")) + (display (hint-string option-string option-list #:fieldname "option")) + (throw 'bad! option-string)) + ;; error, if duplicate option + ((assoc-ref %sanitized-options option-string) + (report-error (G_ "")) + (display (string-append "(opensmtpd-match (options ...)) can " + "only have one (opensmtpd-option (option " + "\"" option-string "\")), but more " + " \n are present.\n")) + (display-hint + (format #f (G_ "Try removing one (opensmtpd-option (option \"~a\")).~%") + option-string)) + (throw 'bad! option-record)) + ;; error, if duplicate from or duplicate for option + ((or + (if (and (string=? "for" (substring option-string 0 3)) + (assoc-ref %sanitized-options "for")) + #t + #f) + (if (and (>= (length (string->list option-string)) 4) + (string=? "from" (substring option-string 0 4)) + (assoc-ref %sanitized-options "from")) + #t + #f)) + (throw-error %options + (list "(opensmtpd-match (options ...)) can only have one" + " (option \"for ...\") and one (option \"from ...\")" + "\nBut (option \"" option-string "\") and (option \"" + (opensmtpd-option-option + (if (assoc-ref %sanitized-options "for") + (assoc-ref %sanitized-options "for") + (assoc-ref %sanitized-options "from"))) + "\") are present.\n") + #:hint-strings + (list "Try removing one " + (if (string=? "for" (substring option-string 0 3)) + "(opensmtpd-option (option \"for ...\"))" + "(opensmtpd-option (option \"from ...\"))") + " record.\n"))) + ;; these 3 options must have fieldname data defined. + ((and (member option-string + (list "helo" "mail-from" "rcpt-to")) + (not (opensmtpd-option-data option-record))) + (report-error (G_ "")) + (display (string-append "(option \"" option-string + "\") must have (data ...) of type string or an " + "(opensmtpd-table ...) record.\n")) + (throw 'bad! option-string)) + ;; fieldname data must be a string. + ((and (string=? "tag" option-string) + (not (string? (opensmtpd-option-data option-record)))) + (throw-error option-record + (list "(opensmtpd-match ... (option \"tag\"))" + " must have a 'data' of type string.\n"))) + ((or (string=? "tls" option-string) + (string=? "for" (substring option-string 0 3)) + (string=? "from" (substring option-string 0 4))) + ;; let's test the "for" and "from" options now. + (cond + ;; the options in this list cannot define 'data' or 'regex' + ;; fieldnames. + ((and (member option-string (list "for local" "for any" + "from any" "from local" + "from socket" "tls")) + (or (opensmtpd-option-data option-record) + (opensmtpd-option-regex option-record))) + (report-error (G_ "")) + (display (string-append "When (openmstpd-option (option \"" + option-string "\") ...), " + "then (data ...) and (regex ...) " + "must be #f. \n")) + (throw 'bad! option-record)) + ;; the options in this list must have a data field of type + ;; string or tables-data-are-a-list-of-strings? + ((and (member option-string + (list "for domain" "for rcpt-to" + "from mail-from" "from src")) + (or (false? (opensmtpd-option-data option-record)) + (tables-data-are-assoc-list? + (opensmtpd-option-data option-record)))) + (throw-error option-record + (list "When (openmstpd-option (option \"" + option-string "\") ...) \n" + "then (data ...) must be a string or an \n" + "(opensmtpd-table ....) record whose " + "'data' is a list of strings.\n"))) + (else + (loop (cdr %traversing-options) + (alist-cons + (cond ((string=? "for" (substring option-string 0 3)) + "for") + ((string=? "tls" option-string) + "tls") + (else "from")) + option-record + %sanitized-options) + ;;option-record + (if (null? (cdr %traversing-options)) + '() + (car (cdr %traversing-options))) + ;; option-string + (if (null? (cdr %traversing-options)) + '() + (opensmtpd-option-option + (car (cdr %traversing-options)))))))) + ;; TODO if auth's 'data' is an assoc-list table, then + ;; it IS invalid! + ;; option-string = 'auth' cannot be made invalidly, + ;; do not test for it. + (else + (loop (cdr %traversing-options) + (alist-cons option-string option-record + %sanitized-options) + ;;option-record + (if (null? (cdr %traversing-options)) + '() + (car (cdr %traversing-options))) + ;; option-string + (if (null? (cdr %traversing-options)) + '() + (opensmtpd-option-option + (car (cdr %traversing-options))))))))) + +(define (filter-phase-has-message-and-value? record) + (and (opensmtpd-filter-phase-message record) + (opensmtpd-filter-phase-value record))) + +;; return #t if phase needs a message. Or if the message did not start +;; with a 4xx or 5xx status code. otherwise #f +(define (filter-phase-decision-lacks-proper-message? record) + (define decision (opensmtpd-filter-phase-decision record)) + (if (member decision (list "disconnect" "reject")) + ;; this message needs to be RFC compliant, meaning + ;; that it need to start with 4xx or 5xx status code + (cond ((eq? #f (opensmtpd-filter-phase-message record)) + #t) + ((string? (opensmtpd-filter-phase-message record)) + (let ((number (string->number + (substring + (opensmtpd-filter-phase-message record) 0 3)))) + (if (and (number? number) + (and (< number 600) (> number 399))) + #f + #t)))) + #f)) + +;; 'decision' "rewrite" requires 'value' to be a number. +(define (filter-phase-lacks-proper-value? record) + (define decision (opensmtpd-filter-phase-decision record)) + (if (string=? "rewrite" decision) + (if (and (number? (opensmtpd-filter-phase-value record)) + (eq? #f (opensmtpd-filter-phase-message record))) + #f + #t) + #f)) + +;; 'decision' "junk" or "bypass" cannot have a message or a value. +(define (filter-phase-has-incorrect-junk-or-bypass? record) + (and + (member + (opensmtpd-filter-phase-decision record) + (list "junk" "bypass")) + (or + (opensmtpd-filter-phase-value record) + (opensmtpd-filter-phase-message record)))) + +(define (filter-phase-junks-after-commit? record) + (and (string=? "junk" (opensmtpd-filter-phase-decision record)) + (string=? "commit" (opensmtpd-filter-phase-phase record)))) + +;; returns #t if list is a unique list of or +;; returns # otherwise +;; only opensmtpd-filter-chain? uses this function, and opensmtpd-filter-chain +;; is NEVER actually used. +;; I could possibly remove it. +(define (list-of-unique-filter-or-filter-phase? %filters) + (and (list? %filters) + (not (null? %filters)) + ;; this list is made up of only + ;; or + (every (lambda (filter) + (or (opensmtpd-filter? filter) + (opensmtpd-filter-phase? filter))) + %filters) + ;; each filter-name is unique. + (not (duplicate-filter-name %filters)))) + +(define (filters->list-of-filter-names %filters) + (map (lambda (filter) + (cond ((opensmtpd-filter-phase? filter) + (opensmtpd-filter-phase-name filter)) + (else (opensmtpd-filter-name filter)))) + %filters)) + +(define (duplicate-string-in-list strings) + (define first-string (car strings)) + (cond ((null? (cdr strings)) + #f) + ((any (lambda (element) + (if (string=? element first-string) + element + #f)) + (cdr strings)) + first-string) + (else (duplicate-string-in-list (cdr strings))))) + +(define (duplicate-filter-name %filters) + (define filter-names (filters->list-of-filter-names %filters)) + (duplicate-string-in-list filter-names)) + +;; the sanitize procedures used for sanitizing each and +;; fieldname 'filters'. +;; It primarily sanitizes . The only sanitization it does +;; for s, is no make sure there are no duplicate filter names. + +(define* (sanitize-socket-and-interfaces-filters + %filters + #:key (socket-or-interface "interface")) + + ;; if there are two filters with the same name, store that name here. + (define the-duplicate-filter-name + (if (not %filters) + #f + (duplicate-filter-name %filters))) + + (define %filter-phases + (if (not %filters) + '() + (remove opensmtpd-filter? %filters))) + ;; the order of the first two tests in this cond is important. + ;; (false?) has to be 1st and (duplicate-filter-filter-name) has to be + ;; second. You may optionally re-order the other alternates in the cond. + (cond ((false? %filters) + #f) + (the-duplicate-filter-name + (report-error (G_ "")) + (display (string-append + "(opensmtpd-" socket-or-interface + " (filters ...)) has a duplicate filter name: \"" + the-duplicate-filter-name "\".\n")) + (throw 'bad! %filters)) + (else + (let loop ((%traversing-list %filter-phases) + (fieldname (if (null? %filter-phases) + '() + (opensmtpd-filter-phase-decision + (car %filter-phases))))) + (cond + ((null? %traversing-list) + %filters) + ((opensmtpd-filter? (car %traversing-list)) + (loop (cdr %traversing-list) + (if (null? (cdr %traversing-list)) + '() + (opensmtpd-filter-phase-decision + (car (cdr %traversing-list)))))) + ((filter-phase-has-message-and-value? + (car %traversing-list)) + (report-error (G_ "")) + (display + (string-append "(opensmtpd-filter-phase ...) cannot define " + "fieldnames 'value' \n and 'message'.\n"))) + ((filter-phase-decision-lacks-proper-message? + (car %traversing-list)) + (cond + ((string? fieldname) + (report-error + (G_ "(decision \"~a\") with (message ...) is invalid.~%") + fieldname)) + ((or (integer? fieldname) (boolean? fieldname)) + (report-error + (G_ "(decision ~a) with (message ...) is invalid.~%") + fieldname)) + (else + (report-error + (G_ "(~a ...\") with (message ...) is invalid.~%... is ~a") + fieldname))) + (display-hint + (G_ (string-append "Try (opensmtpd-filter-phase " + "(message \"406 Not acceptable.\") " + "(decision \"" fieldname "\")).\n"))) + (throw 'bad! (car %traversing-list))) + ((filter-phase-lacks-proper-value? (car %traversing-list)) + (begin + (report-error (G_ "")) + (display + (string-append + "(opensmtpd-filter-phase (decision \"rewrite\")" + "\n\t\t(value ...)) must be a number.\n")) + (display-hint (G_ "Try (value 5).")) + (throw 'bad! (car %traversing-list)))) + ((filter-phase-has-incorrect-junk-or-bypass? + (car %traversing-list)) + (begin + (report-error (G_ "")) + (display + (string-append "(opensmtpd-filter-phase (decision \"" + (opensmtpd-filter-phase-decision + (car %traversing-list)) + "\") cannot define (message ...) or " + "(value ...).\n")) + (throw 'bad! (car %traversing-list)))) + ((filter-phase-junks-after-commit? (car %traversing-list)) + (begin + (report-error (G_ "")) + (display + (string-append + "(opensmtpd-filter-phase (decision \"junk\")\n\t\t " + "(phase \"commit\")) is invalid.\n")) + (display-hint + (G_ (string-append "You cannot junk an email during phase " + "\"commit\". Try (phase \"data\").\n"))) + (throw 'bad! (car %traversing-list)))) + (else (loop (cdr %traversing-list) + (if (null? (cdr %traversing-list)) + '() + (opensmtpd-filter-phase-decision + (car (cdr %traversing-list))))))))))) + +(define* (sanitize-options-for-filter-phase %options) + (define option-list + (list "fcrdns" "rdns" "src" "helo" "auth" "mail-from" "rcpt-to")) + (let loop ((%traversing-options %options) + ;; sanitized-options is an alist that may end of looking like: + ;; (("fcrdns" (opensmtpd-option (option "fcrdns"))) + ;; ("auth" (opensmtpd-option (option "auth")))) + (%sanitized-options '()) + (option-record (if (null? %options) + '() + (car %options))) + (option-string (if (null? %options) + '() + (opensmtpd-option-option (car %options))))) + (cond + ((null? %traversing-options) + %options) + ;; error if option-string is invalid option + ((not (member option-string option-list)) + (report-error + (G_ "(opensmtpd-filter-phase (option \"~a\")) is invalid.\n") + option-string) + (display-hint (G_ "")) + (display (hint-string option-string option-list + #:fieldname "option")) + (throw 'bad! option-string)) + + ;; if we see two "rdns" (for example), throw a + ;; "duplicate option" error. + ((assoc-ref %sanitized-options option-string) + (report-error (G_ "")) + (display (string-append "(opensmtpd-filter-phase (options ...)) can " + "only have one\n (opensmtpd-option (option \"" + option-string "\")), but more are present.\n")) + (display-hint + (format #f (G_ "Try removing one (option \"~a\").~%") option-string)) + (throw 'bad! option-record)) + + ;; the next 4 options must have fieldname 'data' defined. + ((and (member option-string + (list "src" "helo" "mail-from" "rcpt-to")) + (not (opensmtpd-table? (opensmtpd-option-data option-record)))) + (report-error (G_ "")) + (display (string-append "(opensmtpd-filter-phase ... " "(option \"" + option-string "\")) must define (data ...).\n")) + (display-hint (G_ "Try defining (data (opensmtpd-table ...).\n")) + (throw 'bad! option-record)) + ;;fcrdns cannot have fieldname data defined + ((and (string=? "fcrdns" option-string) + (opensmtpd-option-data option-record)) + (report-error (G_ "")) + (display (string-append "(opensmtpd-option \"" option-string "\") " + "cannot define (data ...).\n")) + (display-hint (G_ "")) + (display "Try removing (data ...).\n") + (throw 'bad! option-record)) + ;; rdns and auth cannot be made invalidly. + ;; skip testing them. + (else (loop (cdr %traversing-options) + (alist-cons option-string option-record + %sanitized-options) + ;; option-record + (if (null? (cdr %traversing-options)) + '() + (car (cdr %traversing-options))) + ;; option-string + (if (null? (cdr %traversing-options)) + '() + (opensmtpd-option-option + (car (cdr %traversing-options))))))))) + +(define* (throw-error var %strings + #:key + (record-name #f) + (duplicate-option #f) + (fieldname #f) + (hint-strings #f)) + (cond ((and record-name fieldname) + (cond ((or (string? var)) + (report-error (G_ "(~a \"~a\") is invalid.~%") fieldname var)) + ((boolean? var) + (report-error (G_ "(~a ~a) is invalid.~%") fieldname var)) + ((number? var) + (report-error (G_ "(~a ~a) is invalid.~%") fieldname + (number->string var))) + (else + (report-error (G_ "(~a ...) is invalid.~%Its value is: ~a~%") + fieldname var))) + (display-hint (G_ (string-append "(opensmtpd-" record-name + " (fieldname " fieldname "...)) " + (apply string-append %strings)))) + (throw 'bad! var)) + ((list? hint-strings) + (report-error (G_ "")) + (display (apply string-append %strings)) + (display-hint (G_ (apply string-append hint-strings))) + (throw 'bad! var)) + ;; display the output for throw-error-duplicate-option + (duplicate-option + (report-error (G_ "")) + (display (apply string-append %strings)) + (display-hint + (format #f + (G_ "Try removing one (opensmtpd-option \"~a\") option.\n") + var)) + (throw 'bad! var)) + (else + (report-error (G_ "")) + (display (apply string-append %strings)) + (throw 'bad! var)))) + +;; if strings is (list "auth" "for any" "from local") +;; Then this will return "Try \"auth\", \"for any\", or \"from local\". +(define (try-string strings) + (string-append "Try " + (let loop ((strings strings)) + (cond ((= 1 (length strings)) + (string-append + "or \"" (car strings) "\".\n")) + (else + (string-append + "\"" (car strings) "\", " + (loop (cdr strings)))))))) + +;; suppose string is "for anys" +;; and strings is (list "for any" "for local" "for domain") +;; then hint-string will return "Did you mean "for any"?" +(define* (hint-string string strings + #:key (fieldname #f)) + (define str (string-closest string strings)) + (if (not str) + (try-string strings) + (if fieldname + (string-append "Did you mean (" fieldname " \"" + str "\") ?\n") + (string-append "Did you mean \"" str "\" ?\n")))) + +;; this is used for sanitizing fieldname 'options' +(define (contains-duplicate? list) + (if (null? list) + #f + (or + ;; check if (car list) is in (cdr list) + (any (lambda (var) + (equal? var (car list))) + (cdr list)) + ;; check if (cdr list) contains duplicate + (contains-duplicate? (cdr list))))) + +(define* (variable->string var #:key (append "") (postpend " ")) + (let ((var (if (number? var) + (number->string var) + var))) + (if var + (string-append append var postpend) + ""))) + +;;; Various functions to check that lists are of the appropriate type. + +;; given a list and procedure, this tests that each element of list is of type +;; ie: (list-of-type? list string?) tests each list is of type string. +(define (list-of-type? list proc?) + (if (and (list? list) + (not (null? list))) + (let loop ((list list)) + (if (null? list) + #t + (if (proc? (car list)) + (loop (cdr list)) + #f))) + #f)) + +(define (list-of-strings? list) + (list-of-type? list string?)) + +(define (list-of-interface? list) + (list-of-type? list opensmtpd-interface?)) + +(define (list-of-opensmtpd-option? list) + (list-of-type? + list opensmtpd-option?)) + +(define (list-of-opensmtpd-ca? list) + (list-of-type? list opensmtpd-ca?)) + +(define (list-of-opensmtpd-pki? list) + (list-of-type? list opensmtpd-pki?)) + +(define (list-of-opensmtpd-match? list) + (list-of-type? list opensmtpd-match?)) + +(define* (list-of-strings->string list + #:key + (string-delimiter ", ") + (postpend "") + (append "") + (drop-right-number 2)) + (string-drop-right + (string-append (let loop ((list list)) + (if (null? list) + "" + (string-append append (car list) postpend + string-delimiter + (loop (cdr list))))) + append) + drop-right-number)) + +;; TODO I should probably change this to alist, because that's what this is. +(define (assoc-list? assoc-list) + (list-of-type? assoc-list + (lambda (pair) + (and (pair? pair) + (string? (car pair)) + (string? (cdr pair)) + (<= 1 (string-length (car pair))) + (<= 1 (string-length (cdr pair))))))) + +(define (nested-list? list) + (every (lambda (element) + (and + (list-of-strings? element) + (< 1 (length element)))) + list)) + +;; this procedure takes in one argument. +;; if that argument is an whose fieldname 'values' is +;; an assoc-list, then it returns #t, #f if otherwise. +(define (tables-data-are-assoc-list? table) + (if (not (opensmtpd-table? table)) + #f + (assoc-list? (opensmtpd-table-data table)))) + +;; this procedure takes in one argument +;; if that argument is an whose fieldname 'values' is a +;; list of strings, then it returns #t, #f if otherwise. +(define (tables-data-are-a-list-of-strings? table) + (if (not (opensmtpd-table? table)) + #f + (and (list-of-strings? (opensmtpd-table-data table))))) + +;; This procedures takes in an +;; if that table a list of lists of strings eg: +;; (list (list "cat") (list "dog")) +;; then this returns #t, otherwise false. +(define (tables-data-are-a-nested-list-of-strings? table) + (cond ((false? (opensmtpd-table-data table)) + #f) + ((not (list? (opensmtpd-table-data table))) + #f) + (else + (nested-list? (opensmtpd-table-data table))))) + +;;; The following functions convert various records into strings. + +;; these next few functions help me to turn
s +;; into strings suitable to fit into "opensmtpd.conf". +(define (assoc-list->string assoc-list) + (string-drop-right + (let loop ((assoc-list assoc-list)) + (if (null? assoc-list) + "" + ;; pair is (cons "hello" "world") -> ("hello" . "world") + (let ((pair (car assoc-list))) + (string-append + "\"" (car pair) "\"" + " = " + "\"" (cdr pair) "\"" + ", " + (loop (cdr assoc-list)))))) + 2)) + +;; can be of type: (quote list-of-strings) or (quote assoc-list) +;; this will output a string that looks like: +;; table <"mytable"> { "ludo"="ludo@gnu.org" } +(define (opensmtpd-table->string table) + (string-append "table \"" (opensmtpd-table-name table) "\" " + (cond ((tables-data-are-a-list-of-strings? table) + (string-append "{ " + (list-of-strings->string + (opensmtpd-table-data table) + #:append "\"" + #:drop-right-number 3 + #:postpend "\"") " }")) + ((tables-data-are-assoc-list? table) + (string-append "{ " + (assoc-list->string + (opensmtpd-table-data table)) " }")) + (else (throw 'youMessedUp table))) + " \n")) + +;; will output something like: +;; <"mytable"> +(define (opensmtpd-table-name->string table) + (string-append "<\"" (opensmtpd-table-name table) "\">")) + +(define (opensmtpd-interface->string record) + (string-append + "listen on " + (opensmtpd-interface-interface record) " " + (let* ((hostname (opensmtpd-interface-hostname record)) + (hostnames (if (opensmtpd-interface-hostnames record) + (opensmtpd-table-name + (opensmtpd-interface-hostnames record)) + #f)) + (filters (opensmtpd-interface-filters record)) + (filter-name (if filters + (if (< 1 (length filters)) + (generate-filter-chain-name filters) + (if (opensmtpd-filter? (car filters)) + (opensmtpd-filter-name (car filters)) + (opensmtpd-filter-phase-name + (car filters)))) + #f)) + (mask-src (opensmtpd-interface-mask-src record)) + (tag (opensmtpd-interface-tag record)) + (senders (opensmtpd-interface-senders record)) + (masquerade (opensmtpd-interface-masquerade record)) + (secure-connection (opensmtpd-interface-secure-connection record)) + (port (opensmtpd-interface-port record)) + (pki (opensmtpd-interface-pki record)) + (auth (opensmtpd-interface-auth record)) + (auth-optional (opensmtpd-interface-auth-optional record))) + (string-append + (if mask-src + (string-append "mask-src ") + "") + (variable->string hostname #:append "hostname ") + (variable->string hostnames #:append "hostnames <" #:postpend "> ") + (variable->string filter-name #:append "filter \"" #:postpend "\" ") + (variable->string tag #:append "tag \"" #:postpend "\" ") + (if secure-connection + (cond ((string=? "smtps" secure-connection) + "smtps ") + ((string=? "tls" secure-connection) + "tls ") + ((string=? "tls-require" secure-connection) + "tls-require ") + ((string=? "tls-require-verify" secure-connection) + "tls-require verify ")) + "") + (if senders + (string-append "senders <\"" (opensmtpd-table-name senders) "\"> " + (if masquerade + "masquerade " + "")) + "") + (variable->string port #:append "port " #:postpend " ") + (if pki + (variable->string (opensmtpd-pki-domain pki) #:append "pki ") + "") + (if auth + (string-append "auth " + (if (opensmtpd-table? auth) + (string-append + (opensmtpd-table-name->string auth)) + "")) + "") + (if auth-optional + (string-append "auth-optional " + (if (opensmtpd-table? auth-optional) + (string-append + "<\"" + (opensmtpd-table-name->string auth-optional) + "\">") + "")) + "") + "\n")))) + +(define (opensmtpd-socket->string record) + (string-append + "listen on socket " + (let* ((filters (opensmtpd-socket-filters record)) + (filter-name (if filters + (if (< 1 (length filters)) + (generate-filter-chain-name filters) + (if (opensmtpd-filter? (car filters)) + (opensmtpd-filter-name (car filters)) + (opensmtpd-filter-phase-name + (car filters)))) + #f)) + (mask-src (opensmtpd-socket-mask-src record)) + (tag (opensmtpd-socket-tag record))) + (string-append + (if mask-src + (string-append "mask-src ") + "") + (variable->string filter-name #:append "filter \"" #:postpend "\" ") + (variable->string tag #:append "tag \"" #:postpend "\" ") + "\n")))) + +(define (opensmtpd-relay->string record) + (let ((backup (opensmtpd-relay-backup record)) + (backup-mx (opensmtpd-relay-backup-mx record)) + (helo (opensmtpd-relay-helo record)) + ;; helo-src can either be a string IP address or an + (helo-src (if (opensmtpd-relay-helo-src record) + (if (string? (opensmtpd-relay-helo-src record)) + (opensmtpd-relay-helo-src record) + (string-append "<\"" + (opensmtpd-table-name + (opensmtpd-relay-src record)) + "\">")) + #f)) + (domain (if (opensmtpd-relay-domain record) + (opensmtpd-table-name + (opensmtpd-relay-domain record)) + #f)) + (host (opensmtpd-relay-host record)) + (name (opensmtpd-relay-name record)) + (pki (if (opensmtpd-relay-pki record) + (opensmtpd-pki-domain (opensmtpd-relay-pki record)) + #f)) + (srs (opensmtpd-relay-srs record)) + (tls (opensmtpd-relay-tls record)) + (auth (if (opensmtpd-relay-auth record) + (opensmtpd-table-name + (opensmtpd-relay-auth record)) + #f)) + (mail-from (opensmtpd-relay-mail-from record)) + ;; src can either be a string IP address or an + (src (if (opensmtpd-relay-src record) + (if (string? (opensmtpd-relay-src record)) + (opensmtpd-relay-src record) + (string-append "<\"" + (opensmtpd-table-name + (opensmtpd-relay-src record)) + "\">")) + #f))) + + (string-append + "\"" + name + "\" " "relay " + ;;FIXME should I always quote the host fieldname? + ;; do I need to quote localhost via "localhost" ? + (variable->string host #:append "host \"" #:postpend "\" ") + (variable->string backup) + (variable->string backup-mx #:append "backup mx ") + (variable->string helo #:append "helo ") + (variable->string helo-src #:append "helo-src ") + (variable->string domain #:append "domain <\"" #:postpend "\"> ") + (variable->string host #:append "host ") + (variable->string pki #:append "pki ") + (variable->string srs) + (variable->string tls #:append "tls ") + (variable->string auth #:append "auth <\"" #:postpend "\"> ") + (variable->string mail-from #:append "mail-from ") + (variable->string src #:append "src ") + "\n"))) + +(define (opensmtpd-lmtp->string record) + (string-append "lmtp " + (opensmtpd-lmtp-destination record) + (if (opensmtpd-lmtp-rcpt-to record) + (begin + " " (opensmtpd-lmtp-rcpt-to record)) + ""))) + +(define (opensmtpd-mda->string record) + (string-append "mda " + (opensmtpd-mda-command record) " ")) + +(define (opensmtpd-maildir->string record) + (string-append "maildir " + "\"" + (if (opensmtpd-maildir-pathname record) + (opensmtpd-maildir-pathname record) + "~/Maildir") + "\"" + (if (opensmtpd-maildir-junk record) + " junk " + " "))) + +(define (opensmtpd-local-delivery->string record) + (let ((name (opensmtpd-local-delivery-name record)) + (method (opensmtpd-local-delivery-method record)) + (alias (if (opensmtpd-local-delivery-alias record) + (opensmtpd-table-name + (opensmtpd-local-delivery-alias record)) + #f)) + (ttl (opensmtpd-local-delivery-ttl record)) + (user (opensmtpd-local-delivery-user record)) + (userbase (if (opensmtpd-local-delivery-userbase record) + (opensmtpd-table-name + (opensmtpd-local-delivery-userbase record)) + #f)) + (virtual (if (opensmtpd-local-delivery-virtual record) + (opensmtpd-table-name + (opensmtpd-local-delivery-virtual record)) + #f)) + (wrapper (opensmtpd-local-delivery-wrapper record))) + (string-append + "\"" name "\" " + (cond ((string? method) + (string-append method " ")) + ((opensmtpd-mda? method) + (opensmtpd-mda->string method)) + ((opensmtpd-lmtp? method) + (opensmtpd-lmtp->string method)) + ((opensmtpd-maildir? method) + (opensmtpd-maildir->string method))) + ;; FIXME/TODO support specifying alias file:/path/to/alias-file ? + ;; I do not think that is something that I can do... + (variable->string alias #:append "alias <\"" #:postpend "\"> ") + (variable->string ttl #:append "ttl ") + (variable->string user #:append "user ") + (variable->string userbase #:append "userbase <\"" #:postpend "\"> ") + (variable->string virtual #:append "virtual <\"" #:postpend "\"> ") + (variable->string wrapper #:append "wrapper ")))) + +;; this function turns both opensmtpd-local-delivery and +;; opensmtpd-relay into strings. +(define (opensmtpd-action->string record) + (string-append "action " + (cond ((opensmtpd-local-delivery? record) + (opensmtpd-local-delivery->string record)) + ((opensmtpd-relay? record) + (opensmtpd-relay->string record))) + " \n")) + +;; this turns option records found in into strings. +(define* (opensmtpd-option->string record + #:key + (space-after-! #f)) + (let ((bool (opensmtpd-option-bool record)) + (option (opensmtpd-option-option record)) + (regex (opensmtpd-option-regex record)) + (data (opensmtpd-option-data record))) + (string-append + (if (false? bool) + (if space-after-! + "! " + "!") + "") + option " " + (if regex + "regex " + "") + (if data + (if (opensmtpd-table? data) + (string-append + (opensmtpd-table-name->string data) " ") + (string-append data " ")) + "")))) + +(define (opensmtpd-match->string record) + (string-append "match " + (let* ((action (opensmtpd-match-action record)) + (name (cond ((opensmtpd-relay? action) + (opensmtpd-relay-name action)) + ((opensmtpd-local-delivery? action) + (opensmtpd-local-delivery-name action)) + (else 'reject))) + (options (opensmtpd-match-options record))) + (string-append + (if options + (apply string-append + (map opensmtpd-option->string options)) + "") + (if (string? name) + (string-append "action " "\"" name "\" ") + "reject ") + "\n")))) + +(define (opensmtpd-ca->string record) + (string-append "ca " (opensmtpd-ca-name record) " " + "cert \"" (opensmtpd-ca-file record) "\"\n")) + +(define (opensmtpd-pki->string record) + (let ((domain (opensmtpd-pki-domain record)) + (cert (opensmtpd-pki-cert record)) + (key (opensmtpd-pki-key record)) + (dhe (opensmtpd-pki-dhe record))) + (string-append "pki " domain " " "cert \"" cert "\" \n" + "pki " domain " " "key \"" key "\" \n" + (if dhe + (string-append + "pki " domain " " "dhe " dhe "\n") + "")))) + +(define (generate-filter-chain-name list-of-filters) + (string-drop-right (apply string-append + (flatten + (map (lambda (filter) + (list + (if (opensmtpd-filter? filter) + (opensmtpd-filter-name filter) + (opensmtpd-filter-phase-name filter)) + "-")) + list-of-filters))) + 1)) + +(define (opensmtpd-filter->list-of-strings-and-gexps record) + (list "filter " + "\"" (opensmtpd-filter-name record) "\" " + (if (opensmtpd-filter-exec record) + "proc-exec " + "proc ") + "\"" (opensmtpd-filter-proc record) "\"" + "\n\n")) + +;; this procedure takes in a list of and +;; . It returns a string of the form: +;; filter "uniqueName" chain chain { "filter-name", "filter-name2" [, ...]} +(define (opensmtpd-filter-chain->string list-of-filters) + (string-append "filter \"" + (generate-filter-chain-name list-of-filters) + "\" " + "chain {" + (string-drop-right + (apply string-append + (flatten + (map (lambda (filter) + (list + "\"" + (if (opensmtpd-filter? filter) + (opensmtpd-filter-name filter) + (opensmtpd-filter-phase-name filter)) + "\", ")) + list-of-filters))) + 2) + "}\n\n")) + +(define (opensmtpd-filter-phase->string record) + (let ((name (opensmtpd-filter-phase-name record)) + (phase (opensmtpd-filter-phase-phase record)) + (decision (opensmtpd-filter-phase-decision record)) + (options (opensmtpd-filter-phase-options record)) + (message (opensmtpd-filter-phase-message record)) + (value (opensmtpd-filter-phase-value record))) + (string-append "filter " + "\"" name "\" " + "phase " phase " " + "match " + (apply string-append ; turn the options into a string + (flatten + (map (lambda (option) + (opensmtpd-option->string + option #:space-after-! #f)) + options))) + " " + decision " " + (if (member decision (list "reject" "disconnect")) + (string-append "\"" message "\"") + "") + (if (string=? "rewrite" decision) + (string-append "rewrite " (number->string value)) + "") + "\n\n"))) + +;; in the next procedure, the variable 'filters' is a list of +;; , , and filter chains, which are +;; lists that look like: +;; (list (opensmtpd-filter ...) (opensmtpd-filter-phase ...) +;; (opensmtpd-filter-phase ...) (opensmtpd-filter ...)) +;; This function converts (get-opensmtpd-filters ) +;; to a string. +;; Consider if a user passed in a valid , +;; so that (get-opensmtpd-filters (opensmtpd-configuration)) returns +;; (list (opensmtpd-filter +;; (name "rspamd") +;; (proc "rspamd")) +;; ;; this is a listen-on, with a filter-chain. +;; (list (opensmtpd-filter-phase +;; (name "dkimsign") +;; ...) +;; (opensmtpd-filter +;; (name "rspamd") +;; (proc "rspamd")))) +;; +;; (we will call the above list "total filters"): +;; did you notice that filter "rspamd" is listed twice? Once by itself, and +;; once again in a filter chain. How do you make sure that it is NOT printed +;; twice in smtpd.conf? +;; 1st flatten "total filters", then remove its duplicates so that we +;; may print the s and s. +;; 2nd now we go through "total filters", and we only print the filter-chains. +(define (opensmtpd-filters->list-of-strings-and-gexps filters) + ;; first print the unique s and s. + ;; then print the filter-chains. + ;; to do this: flatten filters, then remove duplicates. + (flatten + (list + ;; TODO for funsies, try to figure out how to list the filter-phases and + ;; filters in one go. I tried it earlier, and it broke the service. + ;; Why? + ;; + + ;; print the filter-phases + (apply string-append + (map (lambda (filter) + (cond ((opensmtpd-filter-phase? filter) + (opensmtpd-filter-phase->string filter)) + (else ""))) + (delete-duplicates (flatten filters)))) + + ;; list the filters that may be gexps + (map (lambda (filter) + (cond ((opensmtpd-filter? filter) + (opensmtpd-filter->list-of-strings-and-gexps filter)) + (else ""))) + (delete-duplicates (flatten filters))) + + ;; now we have to print the filter chains. + (apply string-append + (map (lambda (filter) + (cond ((list? filter) + (opensmtpd-filter-chain->string filter)) + (else ; you are a + ""))) + filters))))) + +(define (opensmtpd-configuration-includes->string string) + (string-append + "include \"" string "\"\n")) + +(define (opensmtpd-configuration-srs->string record) + (let ((key (opensmtpd-srs-key record)) + (backup-key (opensmtpd-srs-backup-key record)) + (ttl-delay (opensmtpd-srs-ttl-delay record))) + (string-append + (variable->string key #:append "srs key " #:postpend "\n") + (variable->string backup-key #:append "srs key backup " #:postpend "\n") + (variable->string ttl-delay #:append "srs ttl " #:postpend "\n") + "\n"))) + +;; TODO make sure all options here work! I just fixed limit-max-rcpt! +(define (opensmtpd-smtp->string record) + (let ((ciphers (opensmtpd-smtp-ciphers record)) + (limit-max-mails (opensmtpd-smtp-limit-max-mails record)) + (limit-max-rcpt (opensmtpd-smtp-limit-max-rcpt record)) + (max-message-size (opensmtpd-smtp-max-message-size record)) + (sub-addr-delim (opensmtpd-smtp-sub-addr-delim record))) + (string-append + (variable->string ciphers #:append "smtp ciphers " #:postpend "\n") + (variable->string limit-max-mails + #:append "smtp limit max-mails " #:postpend "\n") + (variable->string limit-max-rcpt + #:append "smtp limit max-rcpt " #:postpend "\n") + (variable->string max-message-size + #:append "smtp max-message-size " #:postpend "\n") + (variable->string sub-addr-delim + #:append "smtp sub-addr-delim " #:postpend "\n") + "\n"))) + +(define (opensmtpd-configuration-queue->string record) + (let ((compression (opensmtpd-queue-compression record)) + (encryption (opensmtpd-queue-encryption record)) + (ttl-delay (opensmtpd-queue-ttl-delay record))) + (string-append + (if compression + "queue compression\n" + "") + (if encryption + (string-append + "queue encryption " + (if (not (boolean? encryption)) + encryption + "") + "\n") + "") + (if ttl-delay + (string-append "queue ttl" ttl-delay "\n") + "")))) + +;; build a list of from +;; opensmtpd-configuration-matches, which is a list of . +;; Each has a fieldname 'action', which accepts +;; an . +(define (get-opensmtpd-actions record) + (define opensmtpd-actions + (let loop ((list (opensmtpd-configuration-matches record))) + (if (null? list) + '() + (cons (opensmtpd-match-action (car list)) + (loop (cdr list)))))) + (delete-duplicates (append opensmtpd-actions))) + +;; build a list of opensmtpd-pkis from +;; opensmtpd-configuration-interfaces and +;; get-opensmtpd-actions +(define (get-opensmtpd-pkis record) + ;; TODO/FIXME/maybe/wishlist could get-opensmtpd-actions -> NOT have an + ;; opensmtpd-relay? + ;; I think so. And if it did NOT have a relay configuration, then + ;; action-pkis would be '() when it needs to be #f. because if the + ;; opensmtpd-configuration has NO pkis, then this function will + ;; return '(), when it should return #f. If it returns '(), then + ;; opensmtpd-configuration-fieldname->string will + ;; print the string "\n" instead of "" + (define action-pkis + (let loop1 ((list (get-opensmtpd-actions record))) + (if (null? list) + '() + (if (and (opensmtpd-relay? (car list)) + (opensmtpd-relay-pki (car list))) + (cons (opensmtpd-relay-pki (car list)) + (loop1 (cdr list))) + (loop1 (cdr list)))))) + ;; FIXME/TODO/maybe/wishlist + ;; this could be #f aka left blank. aka there are no interface records + ;; with pkis. aka there are no lines in the configuration like: + ;; listen on eth0 tls pki smtp.gnucode.me + ;; in that case the smtpd.conf will have an extra "\n" + (define listen-on-pkis + (let loop2 ((list (opensmtpd-configuration-interfaces record))) + (if (null? list) + '() + (if (opensmtpd-interface-pki (car list)) + (cons (opensmtpd-interface-pki (car list)) + (loop2 (cdr list))) + (loop2 (cdr list)))))) + (delete-duplicates (append action-pkis listen-on-pkis))) + +;; takes in a and returns a list whose +;; elements are , , +;; and a filter-chain. +;; It returns a list of and/or +;; here's an example of what this procedure might return: +;; (list (opensmtpd-filter...) (opensmtpd-filter-phase ...) +;; (openmstpd-filter ...) (opensmtpd-filter-phase ...) +;; ;; this next list is a filter-chain. +;; (list (opensmtpd-filter-phase ...) (opensmtpd-filter...))) +;; +;; This procedure handles filter chains a little odd. +(define (get-opensmtpd-filters record) + (define socket-filters + (if (and (opensmtpd-configuration-socket record) + (opensmtpd-socket-filters + (opensmtpd-configuration-socket record))) + (opensmtpd-socket-filters (opensmtpd-configuration-socket record)) + '())) + (define list-of-interfaces + (if (opensmtpd-configuration-interfaces record) + (opensmtpd-configuration-interfaces record) + '())) + + (delete-duplicates + (append + (remove boolean? + (map-in-order + ;; get the filters found in the s + (lambda (interface-or-socket-record) + (if (and + (opensmtpd-interface-filters interface-or-socket-record) + (= 1 (length (opensmtpd-interface-filters + interface-or-socket-record)))) + ;; this next line returns an + (car (opensmtpd-interface-filters + interface-or-socket-record)) + ;; this next line returns a filter chain. + (opensmtpd-interface-filters interface-or-socket-record))) + list-of-interfaces)) + socket-filters))) + +(define (flatten . lst) + "Return a list that recursively concatenates all sub-lists of LST." + (define (flatten1 head out) + (if (list? head) + (fold-right flatten1 out head) + (cons head out))) + (fold-right flatten1 '() lst)) + +;; This function takes in a record, or list, or anything, and returns +;; a list of s assuming the thing you passed into it had +;; any s. +;; +;; is object record? call func on it's fieldnames +;; is object list? loop through it's fieldnames calling func on it's records +;; is object #f or string? or '()? -> #f +(define (get-opensmtpd-tables value) + (delete-duplicates + (remove boolean? + (flatten ;; turn (list '(1) '(2 '(3))) -> '(1 2 3) + (cond ((opensmtpd-table? value) + value) + ((record? value) + (let* ((record-type (record-type-descriptor value)) + (list-of-record-fieldnames + (record-type-fields record-type))) + (map (lambda (fieldname) + (get-opensmtpd-tables + ((record-accessor record-type fieldname) + value))) + list-of-record-fieldnames))) + ((and (list? value) (not (null? value))) + (map get-opensmtpd-tables value)) + (else #f)))))) + +(define (opensmtpd-configuration-fieldname->string + record fieldname-accessor record->string) + (if (fieldname-accessor record) + (begin + (string-append + (list-of-records->string (fieldname-accessor record) + record->string) "\n")) + "")) + +(define (list-of-records->string list-of-records record->string) + (string-append + (cond ((not (list? list-of-records)) + (record->string list-of-records)) + (else + (let loop ((list list-of-records)) + (if (null? list) + "" + (string-append + (record->string (car list)) + (loop (cdr list))))))))) + +(define (opensmtpd-configuration->string record) + ;; should I use this named let, or should I give this a name, or + ;; not use it at all... + ;; eg: + ;; (write-all-fieldnames + ;; (list (cons fieldname fieldname->string) + ;; (cons fieldname2 fieldname->string))) + ;; (let loop ([list + ;; (list + ;; (cons opensmtpd-configuration-includes + ;; (lambda (string) + ;; (string-append + ;; "include \"" string "\"\n"))) + ;; (cons opensmtpd-configuration-smtp opensmtpd-smtp->string) + ;; (cons opensmtpd-configuration-srs opensmtpd-srs->string))]) + ;; (if (null? list) + ;; "" + ;; (string-append + ;; (opensmtpd-configuration-fieldname->string record + ;; (caar list) + ;; (cdar list)) + ;; (loop (cdr list))))) + (string-append + (opensmtpd-configuration-fieldname->string + record opensmtpd-configuration-bounce + (lambda (%bounce) + (if %bounce + (list-of-strings->string %bounce) + ""))) + (opensmtpd-configuration-fieldname->string record + opensmtpd-configuration-smtp + opensmtpd-smtp->string) + (opensmtpd-configuration-fieldname->string + record + opensmtpd-configuration-srs + opensmtpd-configuration-srs->string) + (opensmtpd-configuration-fieldname->string + record + opensmtpd-configuration-queue + opensmtpd-configuration-queue->string) + ;; write out the mta-max-deferred + (opensmtpd-configuration-fieldname->string + record opensmtpd-configuration-mta-max-deferred + (lambda (var) + (string-append "mta max-deferred " + (number->string + (opensmtpd-configuration-mta-max-deferred record)) + "\n"))) + ;;write out all the tables + (opensmtpd-configuration-fieldname->string record get-opensmtpd-tables + opensmtpd-table->string) + ;; write out all the cas + (opensmtpd-configuration-fieldname->string record + opensmtpd-configuration-cas + opensmtpd-ca->string) + ;; write out all the pkis + (opensmtpd-configuration-fieldname->string record get-opensmtpd-pkis + opensmtpd-pki->string) + ;; write all of the interface and socket records + (opensmtpd-configuration-fieldname->string + record + opensmtpd-configuration-interfaces + opensmtpd-interface->string) + (opensmtpd-configuration-fieldname->string record + opensmtpd-configuration-socket + opensmtpd-socket->string) + ;; write all the actions + (opensmtpd-configuration-fieldname->string record get-opensmtpd-actions + opensmtpd-action->string) + ;; write all of the matches + (opensmtpd-configuration-fieldname->string record + opensmtpd-configuration-matches + opensmtpd-match->string))) + +;; FIXME/TODO should I use format here srfi-28 ? +;; web.scm nginx does a (format #f "string" "another string") +;; this could be a list like +;; (list +;; (file-append opensmtpd-dkimsign "/libexec/filter") +;; "-d gnucode.me -s /path/to/selector.cert") +;; Then opensmtpd-configuration->mixed-text-file could be rewritten to be +;; something like +;; (mixed-text-file +;; (eval `(string-append (opensmtpd-configuration-fieldname->string ...)) +;; (gnu services mail))) +(define (opensmtpd-configuration->mixed-text-file record) + (apply mixed-text-file "smtpd.conf" + (flatten (list + ;; write out the includes + (opensmtpd-configuration-fieldname->string + record + opensmtpd-configuration-includes + opensmtpd-configuration-includes->string) + ;; TODO should I change the below line of code into these + ;; two lines of code? + ;;(opensmtpd-configuration-fieldname->string + ;; record get-opensmtpd-filters-and-filter-phases + ;; opensmtpd-filter-and-filter-phase->string) + ;;(opensmtpd-configuration-fieldname->string + ;; record get-opensmtpd-filter-chains + ;; opensmtpd-filter-chain->string) + ;; write out all the filters + (opensmtpd-filters->list-of-strings-and-gexps + (get-opensmtpd-filters record)) + (opensmtpd-configuration->string record))))) + (define %default-opensmtpd-config-file (plain-file "smtpd.conf" " listen on lo @@ -1668,18 +4140,37 @@ (define %default-opensmtpd-config-file match from local for any action outbound ")) -(define opensmtpd-shepherd-service - (match-lambda - (($ package config-file) - (list (shepherd-service +(define (opensmtpd-shepherd-service config) + (list (shepherd-service (provision '(smtpd)) (requirement '(loopback)) (documentation "Run the OpenSMTPD daemon.") - (start (let ((smtpd (file-append package "/sbin/smtpd"))) - #~(make-forkexec-constructor - (list #$smtpd "-f" #$config-file) - #:pid-file "/var/run/smtpd.pid"))) - (stop #~(make-kill-destructor))))))) + (start + (let ((smtpd (file-append + (opensmtpd-configuration-package config) + "/sbin/smtpd"))) + #~(make-forkexec-constructor + (list #$smtpd "-f" + (or + #$(opensmtpd-configuration-config-file config) + #$(opensmtpd-configuration->mixed-text-file config))) + #:pid-file "/var/run/smtpd.pid"))) + (stop #~(make-kill-destructor))))) + +;; TODO why does the below NOT work? +;(define (opensmtpd-shepherd-service config) +; (match-lambda +; (($ package config-file) +; (list (shepherd-service +; (provision '(smtpd)) +; (requirement '(loopback)) +; (documentation "Run the OpenSMTPD daemon.") +; (start (let ((smtpd (file-append package "/sbin/smtpd"))) +; #~(make-forkexec-constructor +; (list #$smtpd "-f" (or #$config-file +; #$(opensmtpd-configuration->mixed-text-file config))) +; #:pid-file "/var/run/smtpd.pid"))) +; (stop #~(make-kill-destructor))))))) (define %opensmtpd-accounts (list (user-group @@ -1700,10 +4191,10 @@ (define %opensmtpd-accounts (home-directory "/var/empty") (shell (file-append shadow "/sbin/nologin"))))) -(define opensmtpd-activation - (match-lambda - (($ package config-file) - (let ((smtpd (file-append package "/sbin/smtpd"))) +(define (opensmtpd-activation config) + (let ((smtpd (file-append (opensmtpd-configuration-package config) "/sbin/smtpd")) + (config-file (opensmtpd-configuration-config-file config)) + (configuration (opensmtpd-configuration->mixed-text-file config))) #~(begin (use-modules (guix build utils)) ;; Create mbox and spool directories. @@ -1711,7 +4202,36 @@ (define opensmtpd-activation (mkdir-p "/var/spool/smtpd") (chmod "/var/spool/smtpd" #o711) (mkdir-p "/var/spool/mail") - (chmod "/var/spool/mail" #o711)))))) + (chmod "/var/spool/mail" #o711) + (display (string-append "checking syntax of " + (or + #$config-file + #$configuration) + "\n")) + (system* #$smtpd "-nf" + (or + #$config-file + #$configuration))))) + +;; TODO why does this not work? +;(define (opensmtpd-activation config) +; (match-lambda +; (($ package config-file) +; (let ((smtpd (file-append package "/sbin/smtpd")) +; (configuration (opensmtpd-configuration->mixed-text-file config))) +; #~(begin +; (use-modules (guix build utils)) + ;; Create mbox and spool directories. +; (mkdir-p "/var/mail") +; (mkdir-p "/var/spool/smtpd") +; (chmod "/var/spool/smtpd" #o711) +; (mkdir-p "/var/spool/mail") +; (chmod "/var/spool/mail" #o711) +; (display (string-append "checking syntax of " +; (or +; #$config-file +; #$configuration) +; "\n"))))))) (define %opensmtpd-pam-services (list (unix-pam-service "smtpd"))) diff --git a/gnu/tests/mail.scm b/gnu/tests/mail.scm index f13751b72f..f9bd96402b 100644 --- a/gnu/tests/mail.scm +++ b/gnu/tests/mail.scm @@ -37,6 +37,7 @@ (define-module (gnu tests mail) #:use-module (guix gexp) #:use-module (guix store) #:use-module (ice-9 ftw) + #:use-module (srfi srfi-64) #:export (%test-opensmtpd %test-exim %test-dovecot @@ -165,6 +166,718 @@ (define %test-opensmtpd (description "Send an email to a running OpenSMTPD server.") (value (run-opensmtpd-test)))) +;; trying to create a bad record, should result in an error. +;; this function should not be able return, instead it should throw an error +;(define (create-bad-record record) + ;; TODO why is this not working +; (with-output-to-port (%make-void-port "w") +; (lambda () (when record #f)))) + +;; if this caller function is reached, then trying to create the bad record +;; resulted in an error. So return true. +(define (return-true error arg) + #t) + +(define (return-false error arg) + #f) + +(define (test-good-record func) + (catch #t func return-false)) + +(define (run-opensmtpd-record-sanitation-test) + ;;(with-output-to-port (%make-void-port "w") + ;; (lambda () + (test-begin "run-opensmtpd-record-sanitation-test") + + (test-error "(interface (filters ...)) has two filters with the same name." + #t + (let ((dkimsign + (opensmtpd-filter + (name "dkimsign") + (exec #t) + (proc + (list + (file-append + opensmtpd-filter-dkimsign + "/libexec/opensmtpd/filter-dkimsign") + " -d gnucode.me -s 2021-09-22 -c " + "relaxed/relaxed -k " + "rando string" + "/etc/dkim/private.key " + "user nobody group nogroup"))))) + (opensmtpd-interface + (interface "lo") + (filters (list + dkimsign + dkimsign))))) + + ;; duplicate filter names. The filters are different, the fix + ;; is to change one filter's 'name' field. + (test-error "Test cannot have two filters with the same name." + #t + (opensmtpd-interface + (filters (list + (opensmtpd-filter-phase + (name "src") + (phase "connect") + (options + (list + (opensmtpd-option + (option "fcrdns") + (bool #f)))) + (decision "junk")) + (opensmtpd-filter-phase + (name "src") + (phase "helo") + (options + (list + (opensmtpd-option + (option "rdns") + (bool #f)))) + (decision "reject") + (message "433 no rdns.")))))) + + (test-error "(filter (proc ...)) is a list of strings or gexps, NOT numbers." + #t + (opensmtpd-filter + (name "dkimsign") + (proc + (list + (file-append opensmtpd-filter-dkimsign + "/libexec/opensmtpd/filter-dkimsign") + " -d gnucode.me -s 2021-09-22 -c relaxed/relaxed -k " + 5 + "/etc/dkim/private.key " + "user nobody group nogroup")) + (exec #t))) + + (test-error "Test fieldname 'phase' has an wrong phase name." + #t + (opensmtpd-filter-phase + (name "filter") + (phase "wrongString") + (decision "bypass") + (options + (list + (opensmtpd-option + (option "auth")))))) + + (test-error + "(filter-phase (decision \"reject\")) requires a 'message'." + #t + (opensmtpd-interface + (filters (list + (opensmtpd-filter-phase + (name "src") + (phase "connect") + (options + (list + (opensmtpd-option + (option "src") + (data (opensmtpd-table + (name "src-table") + (data (list "cat" "hat"))))))) + (decision "reject")))))) + + (test-error (string-append + "Test fieldname 'decision' " + "w/ value \"reject\" and \"disconnect\" requires a 'message'." + " The message must begin with 4xx or 5xx.") + #t + (opensmtpd-interface + (filters (list + (opensmtpd-filter-phase + (name "src") + (phase "connect") + (options + (list + (opensmtpd-option + (option "src") + (data (opensmtpd-table + (name "src-table") + (data (list "cat" "hat"))))))) + (decision "reject") + (message "322 Bad data!")))))) + + (test-error + "(filter-phase \"rewrite\") requires fieldname 'value' to have a number." + #t + (opensmtpd-interface + (filters + (list + (opensmtpd-filter-phase + (name "noFRDNS") + (phase "commit") + (options (list (opensmtpd-option + (option "fcrdns") + (bool #f)))) + (decision "rewrite")))))) + + (define option-string1/2 (list-ref (list "junk" "bypass") + (random 2))) + (test-error + (string-append "(filter-phase \"decision\" with values 'junk' or 'bypass'," + " then fieldname 'message' and 'value' must be blank.") + #t + (opensmtpd-interface + (filters + (list + (opensmtpd-filter-phase + (name "noFRDNS") + (phase "commit") + (options (list (opensmtpd-option + (option "fcrdns") + ))) + (decision option-string1/2) + (message "This is not a good email.")))))) + + (test-error "You cannot junk an email on phase commit." + #t + (opensmtpd-interface + (filters + (list + (opensmtpd-filter-phase + (name "junk-after-commit") + (options (list (opensmtpd-option + (option "fcrdns")))) + (phase "commit") + (decision "junk")))))) + + (define (good-interface1) + (opensmtpd-interface + (senders + (opensmtpd-table + (name "senders") + (data '(("joshua" . "joshua@dismail.de"))))) + (masquerade #t))) + + (test-assert "good interface" (test-good-record good-interface1)) + + (test-error "Test has 2 duplicate options." + #t + (opensmtpd-filter-phase + (name "invalid-fcrdns") + (phase "connect") + (options + (list (opensmtpd-option + (option "fcrdns")) + (opensmtpd-option + (option "fcrdns")))) + (decision "reject") + (message "422 No valid fcrdns."))) + + (test-error "Test option 'src' requires a table." + #t + (opensmtpd-filter-phase + (name "filter") + (phase "helo") + (decision "bypass") + (options + (list + (opensmtpd-option + (option "src")))))) + + (test-error "Test option 'fcrdns' cannot have a table." + #t + (opensmtpd-filter-phase + (name "filter") + (phase "helo") + (decision "bypass") + (options + (list + (opensmtpd-option + (option "fcrdns") + (data (opensmtpd-table + (name "table") + (data (list "hello" "cat"))))))))) + + (test-error "Test must have at least one option." + #t + (opensmtpd-filter-phase + (name "filter") + (phase "helo") + (decision "bypass"))) + + (test-error + "(filter-phase (options ...)) must be a list of s." + #t + (opensmtpd-filter-phase + (name "rdns") + (decision "junk") + (phase "helo") + (options + (list 5)))) + + (test-error "Test (decision \"junks\") is invalid." + #t + (opensmtpd-filter-phase + (name "this") + (options (list + (opensmtpd-option + (option "auth")))) + (phase "helo") + (decision "junks"))) + + (test-error + (string-append + "(local-delivery (virtual ...) must be an " + "whose 'data' is an alist.") + #t + (opensmtpd-local-delivery + (name "receive") + (method (opensmtpd-maildir + (pathname "/home/%{rcpt.user}/Maildir"))) + (virtual (opensmtpd-table + (name "virt") + (data (list "jbranso@dismail.de")))))) + + (test-error "(opensmtpd-match (options ...)) should not be quoted." + #t + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options + '((opensmtpd-option + (option "for any")))))) + + (test-error "Test has duplicate 'for' options." + #t + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options (list + (opensmtpd-option + (option "for any")) + (opensmtpd-option + (option "for local")))))) + + (test-error "Test has duplicate 'from' options." + #t + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options (list + (opensmtpd-option + (option "from any")) + (opensmtpd-option + (option "from auth")))))) + + (define option-string1 + (list-ref (list "helo" "rcpt-to" "mail-from") + (random 3))) + ;; rcpt-to, mail-from, and helo must have a data field. + (test-error (string-append "Test (opensmtpd-option (option \"" + option-string1 + "\")) must also define fieldname 'data'.") + #t + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options (list + (opensmtpd-option + (option option-string1)))))) + (define option-string2 + (list-ref (list "for local" "for any" "from any" + "from local" "from socket" "tls") + (random 6))) + ;; "for local" "for any" "from any" "from local" "from socket" "tls" + ;; cannot have data or regex defined + (test-error (string-append "Test (opensmtpd-option (option \"" + option-string2 + "\")) cannot have fieldname 'data' defined.") + #t + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options (list + (opensmtpd-option + (option option-string2) + (regex #t)))))) + + (test-error "(opensmtpd-match (action ...)) needs to be defined." + #t + (opensmtpd-match + (options (list + (opensmtpd-option + (option "from auth")))))) + + (test-error "(opensmtpd-match (options ...)) has duplicate 'helo's." + #t + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options (list + (opensmtpd-option + (option "helo") + (bool #f)) + (opensmtpd-option + (option "helo")))))) + + (test-error "(opensmtpd-match (options ...)) has duplicate 'mail-from's." + #t + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options (list + (opensmtpd-option + (option "mail-from") + (data "hello")) + (opensmtpd-option + (option "mail-from") + (data "world")))))) + + (test-error + "(opensmtpd-match (options ...)) has an invalid option name: fcrdns." + #t + (opensmtpd-match + (options (list + (opensmtpd-option + (option "fcrdns")))) + (action (opensmtpd-relay + (name "relay"))))) + + (test-error + "(opensmtpd-match (options ...)) has an invalid option name: rdns." + #t + (opensmtpd-match + (options (list + (opensmtpd-option + (option "rdns")))) + (action (opensmtpd-relay + (name "relay"))))) + + (test-error + (string-append + "(opensmtpd-match (options ...)) option-name 'tag' must " + "also have 'data' be a string.") + #t + (opensmtpd-match + (options (list + (opensmtpd-option + (option "tag")))) + (action (opensmtpd-relay + (name "relay"))))) + + (define option-string3 + (list-ref (list "for domain" "for rcpt-to" + "from mail-from" "from src") + (random 4))) + ;; the options in this list: + ;; (list "for domain" "for rcpt-to" "from mail-from" "from src") + ;; must have a data field. + (test-error + (string-append "Test (opensmtpd-option (option \"" + option-string3 "\"))" + " must define fieldname 'data'.") + #t + (opensmtpd-match + (options (list + (opensmtpd-option + (option option-string3)))) + (action (opensmtpd-relay + (name "relay"))))) + + (define option-string4 + (list-ref (list "for local" "for any" "from any" + "from local" "from socket" "tls") + (random 6))) + ;; the options in this list cannot have a data or regex field defined. + ;; (list "for local" "for any" "from any" "from local" "from socket" "tls") + (test-error (string-append "Test (opensmtpd-option (option \"" + option-string4 "\"))" + " cannot define fieldname 'data'.") + #t + (opensmtpd-match + (options (list + (opensmtpd-option + (regex #t) + (option option-string4)))) + (action (opensmtpd-relay + (name "relay"))))) + + + (define option-string5 + (list-ref (list "for domain" "for rcpt-to" + "from mail-from" "from src") + (random 4))) + (test-error (string-append "Test (opensmtpd-option (option \"" + option-string5 "\"))" + " must define fieldname 'data' as a string or " + "an , whose 'data' \n" + "is a list of strings.") + #t + (opensmtpd-match + (options + (list (opensmtpd-option + (option option-string5) + (data (opensmtpd-table + (name "src-table") + (data '(("127.0.0.1" . "374.394.405.23")))))))) + (action (opensmtpd-relay + (name "relay"))))) + + ;; match must have at least one option. + (test-error + "(opensmtpd-match (options ...)) must have at least one ." + #t + (opensmtpd-match + (action + (opensmtpd-local-delivery + (name "mail"))))) + + ;; you cannot have strings of length 0. + (test-error + (string-append + "(opensmtpd-table (name \"table\") (data '((\"james\" . \"\")))) " + "has an empty string.") + #t + (opensmtpd-table + (name "mytable") + (data '(("hello" . ""))))) + + (define good-match1 + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options (list (opensmtpd-option + (option "for any")) + (opensmtpd-option + (option "from any")) + (opensmtpd-option + (option "auth")))))) + + (test-assert "good match" (test-good-record good-match1)) + ;;(test-assert "Test is valid.") + + ;; I used to have some code that assumed if you have + ;; an "" and an "" using the same filter, then that + ;; was a misconfiguration. It's not. + (define (good-opensmtpd-configuration1) + (let ([interface "lo"] + [filter-dkimsign + (opensmtpd-filter + (name "dkimsign") + (exec #t) + (proc (list (file-append opensmtpd-filter-dkimsign + "/libexec/opensmtpd/filter-dkimsign") + " -d gnucode.me -s 2021-09-22 -c relaxed/relaxed -k " + "/etc/dkim/private.key " + "user nobody group nogroup")))]) + (opensmtpd-configuration + (interfaces + (list + ;; send out emails and be sure to dkimsign them. + (opensmtpd-interface + (interface interface) + (filters (list filter-dkimsign))))) + (socket + (opensmtpd-socket + (filters (list filter-dkimsign)))) + (matches (list + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options (list (opensmtpd-option + (option "for any")) + (opensmtpd-option + (option "from any")) + (opensmtpd-option + (option "auth")))))))))) + + (test-assert + (string-append "opensmtpd-configuration may use the same dkimsign " + "filter on and .") + (test-good-record good-opensmtpd-configuration1)) + + ;; this is just the largest configuration that I can test. + (define (good-opensmtpd-configuration2) + (let ([interface "lo"] + [creds-table + (opensmtpd-table + (name "creds") + (data + (list + (cons "joshua" + "$6$Ec4m8FgKjT2F/03Y$k66ABdse9TzCX6qaALB3WBL9GC1rmAWJmaoSjFMpbhzat7DOpFqpnOwpbZ34wwsQYIK8RQlqwM1I/v6vsRq86."))))] + [receive-action + (opensmtpd-local-delivery + (name "receive") + (method (opensmtpd-maildir + (pathname "/home/%{rcpt.user}/Maildir") + (junk #t))) + (alias (opensmtpd-table + (name "aliases") + (data '(("joshua@gnucode.me" . "joshua"))))) + (virtual (opensmtpd-table + (name "virt") + (data '(("josh" + . "jbranso@dismail.de"))))))] + ;; as of 7-24-22 this proc fieldname does not actually work, but + ;; is proper syntax. + [filter-dkimsign (opensmtpd-filter + (name "dkimsign") + (exec #t) + (proc (list + (file-append + opensmtpd-filter-dkimsign + "/libexec/opensmtpd/filter-dkimsign") + " -d gnucode.me -s 2021-09-22 -c " + "relaxed/relaxed -k " + "/etc/dkim/private.key " + "user nobody group nogroup")))] + [filter-invalid-fcrdns (opensmtpd-filter-phase + (name "invalid-fcrdns") + (phase "connect") + (options + (list (opensmtpd-option + (option "fcrdns") + (bool #f)))) + (decision "reject") + (message "422 No valid fcrdns."))] + [filter-invalid-rdns (opensmtpd-filter-phase + (name "invalid-rdns") + (phase "connect") + (options + (list (opensmtpd-option + (option "rdns") + (bool #f)))) + (decision "junk"))] + [smtp.gnucode.me (opensmtpd-pki + (domain "smtp.gnucode.me") + (cert "guix.scm") + (key "guix.scm"))]) + (opensmtpd-configuration + (mta-max-deferred 50) + (queue + (opensmtpd-queue + (compression #t))) + (smtp + (opensmtpd-smtp + (max-message-size "10M"))) + (srs + (opensmtpd-srs + (ttl-delay "5d"))) + (interfaces + (list + (opensmtpd-interface + (interface interface) + (port 25) + (secure-connection "tls") + (filters (list filter-invalid-fcrdns + filter-invalid-rdns)) + (pki smtp.gnucode.me)) + ;; this lets local users logged into the system via ssh send email + ;; be sure to dkimsign them. + (opensmtpd-interface + (interface interface) + (port 465) + (secure-connection "smtps") + (pki smtp.gnucode.me) + (auth creds-table) + (filters (list filter-dkimsign))) + ;; if you uncomment this next line, then you get issues. + ;;(opensmtpd-socket + ;; (filters (list filter-dkimsign))) + ;; send out emails and be sure to dkimsign them. + (opensmtpd-interface + (interface interface) + (port 587) + (secure-connection "tls-require") + (pki smtp.gnucode.me) + (auth creds-table) + (filters (list filter-dkimsign))))) + (socket + (opensmtpd-socket + (filters (list filter-dkimsign)) + (tag "socket"))) + (matches (list + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options (list (opensmtpd-option + (option "for any")) + (opensmtpd-option + (option "from any")) + (opensmtpd-option + (option "auth"))))) + (opensmtpd-match + (action receive-action) + (options (list (opensmtpd-option + (option "from any")) + (opensmtpd-option + (option "for domain") + (data (opensmtpd-table + (name "domain-table") + (data (list "gnucode.me" + "gnu-hurd.com")))))))) + (opensmtpd-match + (action receive-action) + (options (list (opensmtpd-option + (option "for local")))))))))) + + (test-assert "Test my largish example ." + (test-good-record good-opensmtpd-configuration2)) + + ;; the matches have two actions with the same name, + ;; but are different actions. + (test-error + (string-append "Test fieldname 'matches' has " + "two actions with the same name, but the actions are " + "different.") + #t + (opensmtpd-configuration + (matches + (list (opensmtpd-match + (options + (list + (opensmtpd-option + (option "auth")))) + (action + (opensmtpd-local-delivery + (name "my-local-delivery") + (ttl "50m")))) + (opensmtpd-match + (options + (list + (opensmtpd-option + (option "auth")))) + (action + (opensmtpd-local-delivery + (name "my-local-delivery") + (ttl "50h")))))))) + + ;; you can only have 1 opensmtpd-socket. + (test-error + (string-append + "(opensmtpd-configuration> (listen-on ...)) may only have " + "one .") + #t + (let ([interface "lo"]) + (opensmtpd-configuration + (socket + (list + (opensmtpd-socket) + (opensmtpd-socket))) + (matches (list + (opensmtpd-match + (options + (list + (opensmtpd-option + (option "auth")))) + (action (opensmtpd-relay + (name "relay"))))))))) + + (test-end "run-opensmtpd-record-sanitation-test")) + +(define %test-opensmtpd-record-sanitation + (system-test + (name "opensmtpdRecordSanitation") + (description + (string-append " has numerous sanity checks.\n" + "This checks that invalid configurations, return an\n" + "appropriate error.\n")) + (value (run-opensmtpd-record-sanitation-test)))) + (define %exim-os (simple-operating-system -- 2.38.1 From debbugs-submit-bounces@debbugs.gnu.org Fri Dec 23 11:40:07 2022 Received: (at 56046) by debbugs.gnu.org; 23 Dec 2022 16:40:07 +0000 Received: from localhost ([127.0.0.1]:37697 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1p8l5b-00076j-A2 for submit@debbugs.gnu.org; Fri, 23 Dec 2022 11:40:07 -0500 Received: from mx1.dismail.de ([78.46.223.134]:11956) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1p8l5Z-00076G-5m for 56046@debbugs.gnu.org; Fri, 23 Dec 2022 11:40:05 -0500 Received: from mx1.dismail.de (localhost [127.0.0.1]) by mx1.dismail.de (OpenSMTPD) with ESMTP id e529fbc4 for <56046@debbugs.gnu.org>; Fri, 23 Dec 2022 17:39:56 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; d=dismail.de; h=from:to:cc :subject:date:message-id:mime-version:content-type :content-transfer-encoding; s=20190914; bh=2HnqPZ5olFQe8Rdw7tzw2 B9E9ba+BwOdvQlx5L9fGfY=; b=STudcGdFOId7IPo7PQfIqB+yGIRd07nsG3W6i oo9rmIeance+PI6dgwObqwNgKSMs15Bst9+B0MmtsaMhMrFbWosIZt04mOTCJY8N JypshTvX6/q+yAK1Xez3CI6kknefCLG65vUpZJEr1FBjeC80IvAzbwuTK/hg1aRn 7FWYOg03x1fKK1a1kUnloWHlqM6KRAoVva2y8t4ALjwSEuH+wyZ7QjS+dsxJVoXH 2mA4zt2boGE6YcUjujXr0AhgUUtiEbq0wjYCFvYhrBJWF1Fnt6KJwRT6NcYhf/7F P8kN0dUhbGDkECgNvYkmUf5ZrZ1UnLub17WiNAeVCL/0qQ20Q== Received: from smtp1.dismail.de ( [10.240.26.11]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 179392cb for <56046@debbugs.gnu.org>; Fri, 23 Dec 2022 17:39:56 +0100 (CET) Received: from smtp1.dismail.de (localhost [127.0.0.1]) by smtp1.dismail.de (OpenSMTPD) with ESMTP id 85c289a2 for <56046@debbugs.gnu.org>; Fri, 23 Dec 2022 17:39:56 +0100 (CET) Received: by dismail.de (OpenSMTPD) with ESMTPSA id c1994ee4 (TLSv1.3:TLS_AES_256_GCM_SHA384:256:NO); Fri, 23 Dec 2022 17:39:49 +0100 (CET) From: Joshua Branson To: 56046@debbugs.gnu.org Subject: [PATCH opensmtpd-records v4 fixing charset=y error] services (opensmtpd): add opensmtpd records to enhance opensmtpd-configuration. Date: Fri, 23 Dec 2022 11:39:20 -0500 Message-Id: <8001f404c0e54baba71ef9c5536abc54275c4167.1671813513.git.jbranso@dismail.de> X-Mailer: git-send-email 2.38.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Debbugs-Envelope-To: 56046 Cc: Joshua Branson X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" Whoops the last sent patch had some charset=y errors. Hopefully this is better. This is V3 of my opensmtp records configuration. Hopefully soon, I will have this WIP service is guixrus, so more people can easily play with it. https://git.sr.ht/~whereiseveryone/guixrus BUT the new milestone is this, I am now actually able to use opensmtpd on my gnucode.me account! So that is pretty awesome! Here is a link to my server config: https://notabug.org/jbranso/linode-guix-system-configuration/src/master/linode-locke-lamora-current-config.scm The best place to find documentation for this service is here: https://notabug.org/jbranso/linode-guix-system-configuration/src/master/opensmtpd-records-documentation.txt Openmstpd-configuration may only be configured by a config-file that uses the smtpd.conf syntax. This patch, enables one to configure opensmtpd by using record types. * gnu/services/mail.scm: (opensmtpd-table, opensmtpd-ca, opensmtpd-pki, opensmtpd-action-local-delivery, opensmtpd-maildir, opensmtpd-mda, opensmtpd-lmtp, opensmtpd-relay, opensmtpd-option, opensmtpd-filter-phase, opensmtpd-filter, opensmtpd-interface, opensmtpd-socket, opensmtpd-match, opensmtpd-smtp, opensmtpd-srs, opensmtpd-queue, and opensmtpd-configuration): New records. (false?, is-value-right-type, add-comma-or-string, list-of-procedures->string, string-in-list?, my-sanitize, opensmtpd-filter-chain?, throw-error-duplicate-option, sanitize-list-of-options-for-match, sanitize-filters, list-has-duplicates-or-non-filters?, filter-phase-has-message-and-value?, filter-phase-decision-lacks-proper-message?, filter-phase-lacks-proper-value?, filter-phase-has-incorrect-junk-or-bypass?, filter-phase-junks-after-commit?, list-of-unique-filter-or-filter-phase?, throw-error, contains-duplicate?, list-of-type?, list-of-strings?, list-of-unique-opensmtpd-option?, list-of-opensmtpd-ca?, list-of-opensmtpd-pki?, list-of-opensmtpd-listen-on?, list-of-unique-opensmtpd-match?, list-of-strings->string, assoc-list? assoc-list, variable->string, tables-data-are-assoc-list?, tables-data-are-a-list-of-strings?, table-data-are-a-nested-list-of-strings?, assoc-list->string, opensmtpd-table->string, opensmtpd-listen-on->string, opensmtpd-listen-on-socket->string, opensmtpd-action-relay->string, opensmtpd-lmtp->string, opensmtpd-mda->string, opensmtpd-maildir->string, opensmtpd-action-local-delivery->string, opensmtpd-action->string, opensmtpd-option->string, opensmtpd-match->string, opensmtpd-ca->string, opensmtpd-pki->string, generate-filter-chain-name, opensmtpd-filter-chain->string, opensmtpd-filter-phase->string, opensmtpd-filters->string, opensmtpd-listen->string, opensmtpd-srs->string, opensmtpd-smtp->string, opensmtpd-queue->string, get-opensmtpd-actions, get-opensmtpd-pkis, get-opensmtpd-filters, flatten, get-opensmtpd-tables, opensmtpd-fieldname->string, list-of-records->string, opensmtpd->mixed-text-file): New procedures. * gnu/tests/mail.scm : new tests for various opensmtpd records. * doc/guix.texi (OpenSMTPD Service): Added documentation for the new records for opensmtpd. --- doc/guix.texi | 1065 ++++++++++++++++- gnu/services/mail.scm | 2560 ++++++++++++++++++++++++++++++++++++++++- gnu/tests/mail.scm | 713 ++++++++++++ 3 files changed, 4310 insertions(+), 28 deletions(-) diff --git a/doc/guix.texi b/doc/guix.texi index 535c8cdfc3..879a2ad233 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -25407,16 +25407,66 @@ could instantiate a dovecot service like this: @end lisp @subsubheading OpenSMTPD Service +@cindex opensmtpd @deffn {Scheme Variable} opensmtpd-service-type -This is the type of the @uref{https://www.opensmtpd.org, OpenSMTPD} -service, whose value should be an @code{opensmtpd-configuration} object -as in this example: +OpenSMTPD is an easy-to-use mail transfer agent (MTA). OpenSMTPD +@strong{listens} for incoming mail and @strong{matches} the mail to +@strong{actions}. The following records represent those stages: -@lisp -(service opensmtpd-service-type - (opensmtpd-configuration - (config-file (local-file "./my-smtpd.conf")))) +@multitable {aaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @strong{listens} +@tab @code{} +@item +@tab @code{} +@item +@tab +@item @strong{matches} +@tab @code{} +@item +@tab +@item @strong{actions} +@tab @code{} +@item +@tab @code{} +@end multitable + +Additionally, each @code{} and +@code{} may use a list of @code{}, +and/or @code{} records to filter +email/spam. Also numerous records' fieldnames use +@code{} records to hold lists or key value pairs of +data. Be sure to read the @code{} section to learn the +differance between a @code{mapping table} and a @code{list table}. + +Finally, both @code{} and +@code{} use @code{} to +configure various options. + +A simple example opensmtpd configuration is below: + +@lisp +(let ((smtp.gnu.org (opensmtpd-pki + (domain "smtp.gnu.org") + (cert "file.cert") + (key "file.key")))) + (service opensmtpd-service-type + (opensmtpd-configuration + (interfaces (list + (opensmtpd-interface + (pki smtp.gnu.org)) + (opensmtpd-interface + (pki smtp.gnu.org) + (secure-connection "smtps")))) + (matches (list + (opensmtpd-match + (action + (opensmtpd-local-delivery + (name "local-delivery")))) + (opensmtpd-match + (action + (opensmtpd-relay + (name "relay"))))))))) @end lisp @end deffn @@ -25425,7 +25475,7 @@ Data type representing the configuration of opensmtpd. @table @asis @item @code{package} (default: @var{opensmtpd}) -Package object of the OpenSMTPD SMTP server. +Package object of the OpenSMTPD server. @item @code{config-file} (default: @code{%default-opensmtpd-config-file}) File-like object of the OpenSMTPD configuration file to use. By default @@ -25433,14 +25483,1013 @@ it listens on the loopback network interface, and allows for mail from users and daemons on the local machine, as well as permitting email to remote servers. Run @command{man smtpd.conf} for more information. +@item @code{bounce} (default: @code{(list "4h")}) +@code{bounce} is a list of strings, which send warning messages to the +envelope sender when temporary delivery failures cause a message to +remain in the queue for longer than a specified delay. Each delay option +is a string parameter beginning with a positive decimal integer and a +unit, which can be 's', 'm', 'h', or 'd'. At most four delay parameters +can be specified. + +@item @code{interfaces} default: +@lisp +(list + (opensmtpd-interface + (interface "lo") + (port 25))) +@end lisp +@code{interfaces} is a list of @code{} records. +This list details what interfaces and ports OpenSMTPD listens on as well as +other options. + +@item @code{socket} (default: @code{(opensmtpd-socket)}) +Listens for incoming connections on the Unix domain socket. + +@item @code{includes} (default: @code{#f}) +@code{includes} is a list of string filenames. Each filename's contents is +additional configuration that is inserted into the top of the configuration +file. Run @code{man smtpd.conf} for more information. + +@item @code{matches} default: +@lisp +(list (opensmtpd-match + (action (opensmtpd-local-delivery + (name "local") + (method "mbox") + (options + (list + (opensmtpd-option + (option "for local"))))))) + (opensmtpd-match + (action (opensmtpd-relay + (name "outbound"))) + (options + (list + (opensmtpd-option + (option "from local")) + (opensmtpd-option + (option "for any")))))) +@end lisp +@code{matches} is a list of @code{} records, which +matches incoming mail and sends it to a correspending action. The match +records are evaluated sequentially, with the first match winning. +Therefore @emph{the order that you arrange your matches is important}. +It's a good idea to put specific matches first and an all emcompassing +match (like @code{(option "for any")}) @strong{last}. If an incoming +mail does not match any match records, then it is rejected. + +@item @code{mta-max-deferred} (default: @code{100}) +When delivery to a given host is suspended due to temporary failures, cache +at most number envelopes for that host such that they can be delivered as +soon as another delivery succeeds to that host. The default is 100. + +@item @code{queue} (default: @code{#f}) +@code{queue} expects an @code{} record. With it, one may +compress and encrypt queue-ed emails as well as set the default expiration +time for temporarily undeliverable messages. + +@item @code{smtp} (default: @code{#f}) +@code{smtp} expects an @code{} record, which lets one +specifiy how large email may be along with other settings. + +@item @code{srs} (default: @code{#f}) +@code{srs} expects an @code{} record, which lets one set +up SRS, the Sender Rewritting Scheme. + @item @code{setgid-commands?} (default: @code{#t}) Make the following commands setgid to @code{smtpq} so they can be executed: @command{smtpctl}, @command{sendmail}, @command{send-mail}, @command{makemap}, @command{mailq}, and @command{newaliases}. @xref{Setuid Programs}, for more information on setgid programs. + @end table @end deftp +@itemize +@item Data Type: opensmtpd-interface +Data type representing the configuration of an +@code{}. It listens on the fieldname +@code{interface} for incoming connections, using the same syntax as +@code{ifconfig}. The interface parameter may also be an string interface +group, an string IP address, or a string domain name. Listening can +optionally be restricted to a specific address via the fieldname +@code{family}, which can be either @code{"inet4"} or @code{"inet6"}. + +@itemize +@item @code{interface} (default: @code{"lo"}) + +The string interface to listen for incoming connections. This string +may be an interface group, an IP address, or a domain name. These +interfaces can usually be found by the command @code{ip link}. + +@item @code{family} (default: @code{#f}) + +Only listen on a specific address family. Valid strings are +@code{"inet4"} or @code{"inet6"}, which will only listen on IPv4 or IPv6 +respectfully. If @code{(family #f)}, then opensmtpd will listen on both +IPv4 and IPv6. + +@item @code{auth} (default: @code{#f}) +Support SMTPAUTH: clients may only start SMTP transactions after +successful authentication. If @code{auth} is @code{#t}, then users are +authenticated against their own normal login credentials. Alternatively +@code{auth} may be a @code{mapping table} that maps usernames to +encrypted passwords. The password can be encrypted via the +@code{smtpctl} @code{encrypt} subcommand. + +@item @code{auth-optional} (default: @code{#f}) +Support SMTPAUTH optionally: clients need not authenticate, but may do +so. This allows the @code{} to both accept +incoming mail from untrusted senders and permit outgoing mail from +authenticated users. It can be used in situations where it is not +possible to listen on a separate port (usually the submission port, 587) +for users to authenticate. This option also accepts a @code{mapping +table} that maps usernames to encrypted passwords. + +@item @code{filters} (default: @code{#f}) +A list of one or many @code{} or +@code{} records. The filters are applied +sequentially. These records listen and filter on connections handled by this +listener. + +@item @code{hostname} (default: @code{#f}) +Change the default server name in the greeting banner instead of the +default one. + +@item @code{hostnames} (default: @code{#f}) +Override the server name for specific addresses. Use a @code{mapping +table} that maps string IP addresses to string hostnames. If the address +on which the connection arrives appears in the mapping, the associated +hostname is used. + +@item @code{mask-src} (default: @code{#f}) +If @code{#t}, then omit the from part when prepending “Received” headers. + +@item @code{disable-dsn} (default: @code{#f}) +When @code{#t}, then disable the DSN (Delivery Status Notification) extension. + +@item @code{pki} (default: @code{#f}) +For secure connections, use an @code{} record to prove a +mail server's identity. + +@item @code{port} (default: @code{25}) +Listen on the integer port instead of the default port of 25. + +@item @code{proxy-v2} (default: @code{#f}) +If @code{#t}, then support the PROXYv2 protocol, rewriting appropriately source +address received from proxy. + +@item @code{received-auth} (default: @code{#f}) +If @code{#t}, then in “Received” headers, report whether the session was +authenticated and by which local user. + +@item @code{senders} (default: @code{#f}) +Look up the authenticated user in the supplied @code{mapping table} to +find the email addresses that user is allowed to submit mail as. + +@item @code{masquerade} (default: @code{#f}) +@code{masquerade}, is used in conjunction with @code{senders}. If +@code{#t}, then the From header is rewritten to match the sender +provided in the SMTP session. If @code{senders} is @code{#false}, then +@code{masquerade} cannot be @code{#t}. + +@item @code{secure-connection} (default: @code{#f}) +This is a string of one of these options: + +@multitable {aaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @code{"smtps"} +@tab Support SMTPS, by default on port 465. +@item @code{"tls"} +@tab Support STARTTLS, by default on port 25. +@item @code{"tls-require"} +@tab Like @code{"tls"}, but force clients to +@item +@tab establish a secure connection before being +@item +@tab allowed to start an SMTP transaction. +@item @code{"tls-require-verify"} +@tab Like @code{"tls-require"}, but clients must +@item +@tab also provide a valid certificate +@item +@tab to establish an SMTP session. +@end multitable + +@item @code{tag} (default: @code{#f}) +Clients connecting to the listener are tagged with the given string tag. +@end itemize + +@item Data Type: opensmtpd-socket +Data type representing the configuration of an +@code{}. Listen for incoming SMTP connections on the +Unix domain socket @samp{/var/run/smtpd.sock}. This is done by default, +even if the record is absent. + +@itemize +@item @code{filters} (default: @code{#f}) +A list of one or many @code{} or +@code{} records. These filter incoming +connections handled by this listener. + +@item @code{mask-src} (default: @code{#f}) +If @code{#t}, then omit the from part when prepending “Received” headers. + +@item @code{tag} (default: @code{#f}) +Clients connecting to the listener are tagged with the given string tag. +@end itemize + +@item Data Type: opensmtpd-match +@cindex opensmtpd-match +This data type represents the configuration of an +@code{} record. + +If at least one mail envelope matches the options of one match record, +receive the incoming message, put a copy into each matching envelope, +and atomically save the envelopes to the mail spool for later processing +by the respective @code{} found in fieldname +@code{action}. Here is an example @code{opensmtpd-match} +record. + +@lisp +(opensmtpd-match + (action (opensmtpd-local-delivery + (name "receive") + (method (opensmtpd-maildir + (pathname "/home/%@{rcpt.user@}/Maildir") + (junk #t))) + (virtual (opensmtpd-table + (name "virt") + (data '(("carmen" . "carmen@@gnu.org"))))))) + (options (list (opensmtpd-option + (option "from any")) + (opensmtpd-option + (option "for domain") + (data (opensmtpd-table + (name "domain-table") + (data (list "gnu.org" "fsf.org")))))))) +@end lisp + +@itemize +@item @code{action} (default: @code{#f}) + +If mail matches this match configuration, then do this action. Valid values +include @code{} or +@code{}. + +@item @code{options} (default: @code{#f}) +The fieldname @code{option} is a list of unique +@code{} records. + +There are some mutually exclusive options: there can be only one ``for'' +and only one ``from'' option. + +@multitable {aaaaaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@headitem for +@tab from +@item only use one of the following: +@tab only use one of the following: +@item @code{"for any"} +@tab @code{"from any"} +@item @code{"for local"} +@tab @code{"from auth"} +@item @code{"for domain"} +@tab @code{"from local"} +@item @code{"for rcpt-to"} +@tab @code{"from mail-from"} +@item +@tab @code{"from socket"} +@item +@tab @code{"from src"} +@end multitable + +Additionally, some options require additional data via +@code{}'s fieldname @code{data}. The following list +will explain the below syntax. + +@itemize +@item @code{"for any"} +This option only requires fieldname @code{option} to have the string +@code{"for any"}: + +@lisp + (opensmtpd-option + (option "for any")) +@end lisp + +@item @code{"tag"} _tag_ +This option only requires fieldname @code{option} to have the string +@code{"tag"} with a string in fieldname @code{data}: + +@lisp + (opensmtpd-option + (option "tag") + (data "this-tag")) +@end lisp + +@item @code{"for rcpt"} _domain_ | +This option requires fieldname @code{data} to have a string domain or +@code{list table}: + +@lisp + (opensmtpd-option + (option "for rcpt") + (data "gnu.org")) +@end lisp + +OR + +@lisp + (opensmtpd-option + (option "for rcpt") + (data (list "gnu.org" "fsf.org"))) +@end lisp +@end itemize + +The following matching options are supported and can all be negated (via not +#t). The options that support a table (anything surrounded with '<' and '>' +eg:
), also support specifying regex via (regex #t). + +@itemize +@item @code{"for any"} +Specify that session may address any destination. + +@item @code{"for local"} +Specify that session may address any local domain. This is the default, +and may be omitted. + +@item @code{"for domain"} _domain_ | +Specify that session may address the string _domain_ or +@code{list table} . + +@item @code{"for rcpt-to"} _recipient_ | +Specify that session may address the string _recipient_ or list table +. + +@item @code{"from any"} +Specify that session may originate from any source. + +@item @code{"from auth"} +Specify that session may originate from any authenticated user, no matter +the source IP address. + +@item @code{"from auth"} _user_ | +Specify that the session may originate from authenticated _user_ or +@code{list table} , no matter the source IP address. + +@item @code{"from local"} +Specify that session may only originate from a local IP address, or from +the local enqueuer. This is the default, and may be omitted. + +@item @code{"from mail-from"} _sender_ | +Specify that session may originate from _sender_ or @code{list table} +, no matter the source IP address. + +@item @code{"from rdns"} +Specify that session may only originate from an IP address that resolves +to a reverse DNS@. + +@item @samp{"from rdns"} _hostname_ | +Specify that session may only originate from an IP address that resolves +to a reverse DNS matching string _hostname_ or @code{list table} +. + +@item @samp{"from socket"} +Specify that session may only originate from the local enqueuer. + +@item @code{"from src"} _address_ |
+Specify that session may only originate from string _address_ or +@code{list table}
which can be a specific address or a subnet +expressed in CIDR-notation. + +@item @code{"auth"} +Matches transactions which have been authenticated. + +@item @code{"auth"} _username_ | +Matches transactions which have been authenticated for string _user_ or +@code{list table} . + +@item @code{"helo"} _helo-name_ | +Specify that session's HELO / EHLO should match the string _helo-name_ +or @code{list table} . + +@item @code{"mail-from"} _sender_ | +Specify that transactions's MAIL FROM should match the string _sender_ +or @code{list table} . + +@item @code{"rcpt-to"} _recipient_ | +Specify that transaction's RCPT TO should match the string _recipient_ +or @code{list table} . + +@item @code{"tag"} _tag_ +Matches transactions tagged with the given tag. + +@item @code{"tls"} +Specify that transaction should take place in a TLS channel. +@end itemize + +@end itemize + +@item Data Type: opensmtpd-local-delivery +This data type represents the configuration of an +@code{} record. + +@itemize +@item @code{name} (default: @code{#f}) +@code{name} is the string name of the relay action. + +@item @code{method} (default: @code{"mbox"}) +The email delivery option. Valid options are: + +@itemize +@item @code{"mbox"} +Deliver the message to the user's mbox with mail.local(8). + +@item @code{"expand-only"} +Only accept the message if a delivery method was specified in an aliases +or .forward file. + +@item @code{"forward-only"} +Only accept the message if the recipient results in a remote address after +the processing of aliases or forward file. + +@item @code{} +Deliver the message to an LMTP server at @code{}'s +fieldname @code{destination}. The location may be expressed as string +host:port or as a UNIX socket. Optionally, @code{}'s +fieldname @code{rcpt-to} might be specified to use the recipient email +address (after expansion) instead of the local user in the LMTP session +as RCPT TO. + +@item @code{} +Deliver the message to the maildir in +@code{}'s fieldname @code{pathname} if specified, +or by default to @code{"~/Maildir"}. + +The pathname may contain format specifiers that are expanded before use +(see the below section about Format Specifiers). + +If @code{}'s record fieldname @code{junk} is @code{#t}, +then message will be moved to the ‘Junk’ folder if it contains a positive +‘X-Spam’ header. This folder will be created under fieldname @code{pathname} if +it does not yet exist. + +@item @code{} +Delegate the delivery to the @code{}'s fieldname +@code{command} (type string) that receives the message on its standard input. + +The @code{command} may contain format specifiers that are expanded before use +(see Format Specifiers). +@end itemize + +@item @code{alias} (default: @code{#f}) +Use the @code{mapping table} for aliases expansion. + +@item @code{ttl} (default: @code{#f}) +@code{ttl} is a string specify how long a message may remain in the queue. It's +format is @code{n@{s|m|h|d@}}. eg: @code{"4m"} is four minutes. + +@item @code{user} (default: @code{#f} ) +@code{user} is the string username for performing the delivery, to be looked up +with getpwnam(3). + +This is used for virtual hosting where a single username is in charge of +handling delivery for all virtual users. + +This option is not usable with the mbox delivery method. + +@item @code{userbase} (default: @code{#f}) +@code{userbase} is an @code{} record for mapping user +lookups instead of the getpwnam(3) function. + +The fieldnames @code{user} and @code{userbase} are mutually exclusive. + +@item @code{virtual} (default: @code{#f}) +@code{virtual} is an @code{} record is used for virtual +expansion. +@end itemize + +@item Data Type: opensmtpd-relay +This data type represents the configuration of an +@code{} record. + +@itemize +@item @code{name} (default: @code{#f}) +@code{name} is the string name of the relay action. + +@item @code{backup} (default: @code{#f}) +When @code{#t}, operate as a backup mail exchanger delivering messages to any +mail exchanger with higher priority. + +@item @code{backup-mx} (default: @code{#f}) +Operate as a backup mail exchanger delivering messages to any mail exchanger +with higher priority than mail exchanger identified as string name. + +@item @code{helo} (default: @code{#f}) +Advertise string heloname as the hostname to other mail exchangers during +the HELO phase. + +@item @code{helo-src} (default: @code{#f} ) + Use the mapping @code{} to look up a hostname +matching the source address, to advertise during the HELO phase. + +@item @code{domain} (default: @code{#f}) +Do not perform MX lookups but look up destination domain in an +@code{} and use matching relay url as relay host. + +@item @code{host} (default: @code{#f}) +Do not perform MX lookups but relay messages to the relay host described by +the string relay-url. The format for relay-url is +@samp{[proto://[label@@]]host[:port]}. The following protocols are available: + +@multitable {aaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @code{smtp} +@tab Normal SMTP session with opportunistic STARTTLS (the default). +@item @code{smtp+tls} +@tab Normal SMTP session with mandatory STARTTLS@. +@item @code{smtp+notls} +@tab Plain text SMTP session without TLS@. +@item @code{lmtp} +@tab LMTP session. port is required. +@item @code{smtps} +@tab SMTP session with forced TLS on connection, default port is +@item +@tab 465. +@end multitable + +Unless noted, port defaults to 25. + +The label corresponds to an entry in a credentials table, as documented in +@code{man table}. It is used with the @code{"smtp+tls"} and @code{"smtps"} protocols for +authentication. Server certificates for those protocols are verified by +default. + +@item @code{pki} (default: @code{#f}) +For secure connections, use the certificate associated with +@code{} (declared in a pki directive) to prove the +client's identity to the remote mail server. + +@item @code{srs} (default: @code{#f}) +If @code{#t}, then when relaying a mail resulting from a forward, use the Sender +Rewriting Scheme to rewrite sender address. + +@item @code{tls} (default: @code{#f}) boolean or string ``no-verify'' +When @code{#t}, Require TLS to be used when relaying, using mandatory STARTTLS by +default. When used with a smarthost, the protocol must not be +@samp{"smtp+notls://"}. When string @code{"no-verify"}, then do not require a valid +certificate. + +@item @code{auth} (default: @code{#f}) @code{} +Use the alist @code{} for connecting to relay-url +using credentials. This option is usable only with fieldname @code{host} option. + +@item @code{mail-from} (default: @code{#f}) string +Use the string mailaddress as MAIL FROM address within the SMTP transaction. + +@item @code{src} (default: @code{#f}) string | @code{} +Use the string or @code{} sourceaddr for the +source IP address, which is useful on machines with multiple interfaces. If +the list contains more than one address, all of them are used in such a way +that traffic is routed as efficiently as possible. +@end itemize + +@item Data Type: opensmtpd-filter +This data type represents the configuration of an +@code{}. This is the filter record one should use +if they want to use an external package to filter email eg: rspamd or +spamassassin. + +@itemize +@item @code{name} (default: @code{#f}) +The string name of the filter. + +@item @code{proc} (default: @code{#f}) +The string command or process name. If @code{proc-exec} is @code{#t}, @code{proc} is +treated as a command to execute. Otherwise, it is a process name. + +@item @code{proc-exec} (default: @code{#f}) +If @code{#t}, then execute the command in @code{proc}. +@end itemize + +@item Data Type: opensmtpd-filter-phase +This data type represents the configuration of an +@code{}. + +In a regular workflow, @code{smtpd(8)} may accept or reject a message +based only on the content of envelopes. Its decisions are about the +handling of the message, not about the handling of an active session. + +Filtering extends the decision making process by allowing +@code{smtpd(8)} to stop at each phase of an SMTP session, check that +options are met, then decide if a session is allowed to move forward. + +With filtering via an @code{} record, a +session may be interrupted at any phase before an envelope is complete. A +message may also be rejected after being submitted, regardless of whether the +envelope was accepted or not. + +@itemize +@item @code{name} (default: @code{#f}) + +The string name of the filter phase. + +@item @code{phase-name} (default: @code{#f}) +The string name of the phase. Valid values are: + +@multitable {aaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @code{"connect"} +@tab upon connection, before a banner is displayed +@item @code{"helo"} +@tab after HELO command is submitted +@item @code{"ehlo"} +@tab after EHLO command is submitted +@item @code{"mail-from"} +@tab after MAIL FROM command is submitted +@item @code{"rcpt-to"} +@tab after RCPT TO command is submitted +@item @code{"data"} +@tab after DATA command is submitted +@item @code{"commit"} +@tab after message is fully is submitted +@end multitable + +@item @code{options} (default @code{#f}) +A list of unique @code{} records. + +At each phase, various options, specified by a list of +@code{}, may be checked. The +@code{}'s fieldname @code{option} values of: +@code{"fcrdns"}, @code{"rdns"}, and @code{"src"} data are available in +all phases, but other data must have been already submitted before they +are available. Options with a @code{
} next to them require the +@code{}'s fieldname @code{data} to be an +@code{}. There are the available options: + +@multitable {aaaaaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @code{"fcrdns"} +@tab forward-confirmed reverse DNS is valid +@item @code{"rdns"} +@tab session has a reverse DNS +@item @code{"rdns"}
+@tab session has a reverse DNS in table +@item @code{"src"}
+@tab source address is in table +@item @code{"helo"}
+@tab helo name is in table +@item @code{"auth"} +@tab session is authenticated +@item @code{"auth"}
+@tab session username is in table +@item @code{"mail-from"}
+@tab sender address is in table +@item @code{"rcpt-to"}
+@tab recipient address is in table +@end multitable + +These conditions may all be negated by setting +@code{(opensmtpd-option (bool #f))}. + +Any conditions that require a table may indicate that tables include regexs +setting @code{(opensmtpd-option (regex #t))}. + +@item @code{decision} +A string decision to be taken. Some decisions require an @code{message} +or @code{value}. The value and message may be put in the +@code{}'s fieldname @code{data}. Valid strings are: + +@multitable {aaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @code{"bypass"} +@tab the session or transaction bypasses filters +@item @code{"disconnect"} message +@tab the session is disconnected with message +@item @code{"junk"} +@tab the session or transaction is junked, +@item +@tab i.e., an ‘X-Spam: yes’ header is added to +@item +@tab any messages +@item @code{"reject"} message +@tab the command is rejected with message +@item @code{"rewrite"} value +@tab the command parameter is rewritten with value +@end multitable + +Decisions that involve a message require that the message be RFC valid, +meaning that they should either start with a 4xx or 5xx status code. +Descisions can be taken at any phase, though junking can only happen before +a message is committed. + +@item @code{message} (default @code{#f}) +A string message beginning with a 4xx or 5xx status code. + +@item @code{value} (default: @code{#f}) +A number value. @code{value} and @code{message} are mutually exclusive. +@end itemize + +@item Data Type: opensmtpd-option +This data type represents the configuration of an +@code{}, which is used by +@code{} and @code{} +to match various options for email. + +@itemize +@item @code{option} (default @code{#f}) string + +A string option to be taken. Some options require the fieldname +@code{data} to have a string or an @code{}. When the +option record is used inside of an @code{}, then +valid strings for fieldname @code{option} are: + +@itemize +@item @code {"fcrdns"} +@item @code {"rdns"} +@item @code {"src"} +@item @code {"helo"} +@item @code {"auth"} +@item @code {"mail-from"} +@item @code {"rcpt-to"} +@end itemize + +When @code{} is used inside of an +@code{}, then valid strings for fieldname @code{option} +are: + +@itemize +@item @code {"for"} +@item @code {"for any"} +@item @code {"for local"} +@item @code {"for domain"} +@item @code {"for rcpt-to"} +@item @code {"from any"} +@item @code {"from auth"} +@item @code {"from local"} +@item @code {"from mail-from"} +@item @code {"from rdns"} +@item @code {"from socket"} +@item @code {"from src"} +@item @code {"auth"} +@item @code {"helo"} +@item @code {"mail-from"} +@item @code {"rcpt-to"} +@item @code {"tag"} +@item @code {"tls"} +@end itemize + +@item @code{data} (default @code{#f}) string | @code{} +Some options require a string or @code{} to be +present. One would specify that table here. + +@item @code{regex} (default: @code{#f}) boolean +Any options using a table may indicate that tables hold regular +expressions by setting this option to @code{#t}. + +@item @code{bool} (default: @code{#t}) boolean +When @code{(bool #f)}, this option record is negated. +@end itemize + +@item Data Type: opensmtpd-table +This data type represents the configuration of an +@code{}. + +@itemize +@item @code{name} (default @code{#f}) +@code{name} is the name of the @code{} record. + +@item @code{data} (default: @code{#f}) string | list | alist | nested-list +@code{data} expects a string, a list of strings, an alist of strings, or +a nested list of strings. +eg: + +@itemize + +@item string +@lisp +(data "dev@@gnu.org") +@end lisp + +A table of this type is called a @code{string table}. + +@item list +@lisp +(data (list ("gnu.org" "fsf.org"))) +@end lisp + +A table of this type is called a @code{list table}. + +@item alist +@lisp +(data '(("james" . "$encryptedPassword") + ("jennifer" . "$encryptedPassword2))) +@end lisp + +A table of this type is called a @code{mapping table}. + +@item nested-list +@lisp +(data '(("user1" "root@@gnu.org" "admin@@gnu.org") + ("user2" "james@@guix.gnu.org" "sarah@@fsf.org"))) +@end lisp + +A table of this type is also called a @code{mapping table}. + +@end itemize +@end itemize + +@item Data Type: opensmtpd-pki +This data type represents the configuration of an +@code{}. + +@itemize +@item @code{domain} (default @code{#f}) +@code{domain} is the string name of the @code{} record. + +@item @code{cert} (default: @code{#f}) +@code{cert} (default: @code{#f}) + +@code{cert} is the string certificate filename to use for this pki. + +@item @code{key} (default: @code{#f}) +@code{key} is the string certificate falename to use for this pki. + +@item @code{dhe} (default: @code{"none"}) +Specify the DHE string parameter to use for DHE cipher suites with host +pkiname. Valid parameter values are @code{"none"}, @code{"legacy"}, or +@code{"auto"}. For @code{"legacy"}, a fixed key length of 1024 bits is +used, whereas for @code{"auto"}, the key length is determined +automatically. The default is @code{"none"}, which disables DHE cipher +suites. +@end itemize + +@item Data Type: opensmtpd-maildir +@itemize +@item @code{pathname} (default: @code{"~/Maildir"}) +Deliver the message to the maildir if pathname if specified, or by default +to @code{"~/Maildir"}. + +The pathname may contain format specifiers that are expanded before use +(see FORMAT SPECIFIERS). + +@item @code{junk} (default: @code{#f}) +If the junk argument is @code{#t}, then the message will be moved to the @samp{‘Junk’} +folder if it contains a positive @samp{‘X-Spam’} header. This folder will be +created under pathname if it does not yet exist. +@end itemize + +@item Data Type: opensmtpd-mda +This record lets you delegate the delivery to a command that receives +the message on its standard input. + +@itemize +@item @code{name} +The string name for this MDA command. + +@item @code{command} +The command to that delivers the mail. + +The command may contain format specifiers that are expanded before use (see +FORMAT SPECIFIERS). +@end itemize + +@item Data Type: opensmtpd-queue +@itemize +@item @code{compression} (default @code{#f}) +Store queue files in a compressed format. This may be useful to save disk +space. + +@item @code{encryption} (default @code{#f}) +Encrypt queue files with EVP@math{_aes}@math{_256}@math{_gcm}(3). If no key is specified, it is +read with getpass(3). If the string stdin or a single dash (‘-’) is given +instead of a key, the key is read from the standard input. + +@item @code{ttl-delay} (default @code{#f}) +Set the default expiration time for temporarily undeliverable messages, +given as a positive decimal integer followed by a unit s, m, h, or d. The +default is four days (@code{"4d"}). +@end itemize + +@item Data Type: opensmtpd-smtp +Data type representing an @code{} record. + +@itemize +@item @code{ciphers} (default: @code{#f}) +Set the control string for SSL@math{_CTX}@math{_set}@math{_cipher}@math{_list}(3). The default is + ``HIGH:!aNULL:!MD5''. + +@item @code{limit-max-mails} (default: @code{100}) +Limit the number of messages to count for each sessio + +@item @code{limit-max-rcpt} (default: @code{1000}) +Limit the number of recipients to count for each transaction. + +@item @code{max-message-size} (default: @code{35M}) +Reject messages larger than size, given as a positive number of bytes or as +a string to be parsed with scan@math{_scaled}(3). + +@item @code{sub-addr-delim character} (default: @code{+}) +When resolving the local part of a local email address, ignore the ASCII +character and all characters following it. This is helpful for email +filters. @samp{"admin+bills@@gnu.org"} is the same email address as +@samp{"admin@@gnu.org"}. BUT an email filter can filter emails addressed to first +email address into a 'Bills' email folder. +@end itemize + +@item Data Type: opensmtpd-srs +Use this record to set up the Sender Rewriting Scheme (SRS). + +@itemize +@item @code{key} (default: @code{#f}) +Set the secret key to use for SRS, the Sender Rewriting Scheme. + +@item @code{backup-key} (default: @code{#f}) +Set a backup secret key to use as a fallback for SRS@. This can be used to +implement SRS key rotation. + +@item @code{ttl-delay} (default: @code{"4d"}) +Set the time-to-live delay for SRS envelopes. After this delay, a bounce +reply to the SRS address will be discarded to limit risks of forged +addresses. +@end itemize + +@item Format Specifiers +Some configuration records support expansion of their parameters at +runtime. Such records (for example +@code{}, @code{}) may use +format specifiers which are expanded before delivery or relaying. The +following formats are currently supported: + +@multitable {aaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @samp{%@{sender@}} +@tab sender email address, may be empty string +@item @samp{%@{sender.user@}} +@tab user part of the sender email address, may be empty +@item @samp{%@{sender.domain@}} +@tab domain part of the sender email address, may be empty +@item @samp{%@{rcpt@}} +@tab recipient email address +@item @samp{%@{rcpt.user@}} +@tab user part of the recipient email address +@item @samp{%@{rcpt.domain@}} +@tab domain part of the recipient email address +@item @samp{%@{dest@}} +@tab recipient email address after expansion +@item @samp{%@{dest.user@}} +@tab user part after expansion +@item @samp{%@{dest.domain@}} +@tab domain part after expansion +@item @samp{%@{user.username@}} +@tab local user +@item @samp{%@{user.directory@}} +@tab home directory of the local user +@item @samp{%@{mbox.from@}} +@tab name used in mbox From separator lines +@item @samp{%@{mda@}} +@tab mda command, only available for mda wrappers +@end multitable + +Expansion formats also support partial expansion using the optional bracket notations +with substring offset. For example, with recipient domain @samp{“example.org”}: + +@multitable {aaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaa} +@item @samp{%@{rcpt.domain[0]@}} +@tab expands to “e” +@item @samp{%@{rcpt.domain[1]@}} +@tab expands to “x” +@item @samp{%@{rcpt.domain[8:]@}} +@tab expands to “org” +@item @samp{%@{rcpt.domain[-3:]@}} +@tab expands to “org” +@item @samp{%@{rcpt.domain[0:6]@}} +@tab expands to “example” +@item @samp{%@{rcpt.domain[0:-4]@}} +@tab expands to “example” +@end multitable + +In addition, modifiers may be applied to the token. For example, with recipient +@samp{“User+Tag@@Example.org”}: + +@multitable {aaaaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @samp{%@{rcpt:lowercase@}} +@tab expands to “user+tag@@example.org” +@item @samp{%@{rcpt:uppercase@}} +@tab expands to “USER+TAG@@EXAMPLE.ORG” +@item @samp{%@{rcpt:strip@}} +@tab expands to “User@@Example.org” +@item @samp{%@{rcpt:lowercasestrip@}} +@tab expands to “user@@example.org” +@end multitable + +For security concerns, expanded values are sanitized and potentially dangerous +characters are replaced with ‘:’. In situations where they are desirable, the +“raw” modifier may be applied. For example, with recipient +@samp{“user+t?g@@example.org”}: + +@multitable {aaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item @samp{%@{rcpt@}} +@tab expands to “user+t:g@@example.org” +@item @samp{%@{rcpt:raw@}} +@tab expands to “user+t?g@@example.org” +@end multitable +@end itemize + @subsubheading Exim Service @cindex mail transfer agent (MTA) diff --git a/gnu/services/mail.scm b/gnu/services/mail.scm index 43f144a42d..4175cab375 100644 --- a/gnu/services/mail.scm +++ b/gnu/services/mail.scm @@ -35,6 +35,10 @@ (define-module (gnu services mail) #:use-module (gnu packages admin) #:use-module (gnu packages dav) #:use-module (gnu packages tls) + #:use-module (guix i18n) + #:use-module (guix diagnostics) + #:use-module (guix ui) + #:use-module (guix utils) #:use-module (guix records) #:use-module (guix packages) #:use-module (guix gexp) @@ -58,10 +62,149 @@ (define-module (gnu services mail) mailbox-configuration namespace-configuration + opensmtpd-table + opensmtpd-table? + opensmtpd-table-name + opensmtpd-table-data + + opensmtpd-ca + opensmtpd-ca? + opensmtpd-ca-name + opensmtpd-ca-file + + opensmtpd-pki + opensmtpd-pki? + opensmtpd-pki-domain + opensmtpd-pki-cert + opensmtpd-pki-key + opensmtpd-pki-dhe + + opensmtpd-local-delivery + opensmtpd-local-delivery? + opensmtpd-local-delivery-method + opensmtpd-local-delivery-alias + opensmtpd-local-delivery-ttl + opensmtpd-local-delivery-user + opensmtpd-local-delivery-userbase + opensmtpd-local-delivery-virtual + opensmtpd-local-delivery-wrapper + + opensmtpd-maildir + opensmtpd-maildir? + opensmtpd-maildir-pathname + opensmtpd-maildir-junk + + opensmtpd-mda + opensmtpd-mda-name + opensmtpd-mda-command + + opensmtpd-lmtp + opensmtpd-lmtp-destination + opensmtpd-lmtp-rcpt + + opensmtpd-relay + opensmtpd-relay? + opensmtpd-relay-name + opensmtpd-relay-backup + opensmtpd-relay-backup-mx + opensmtpd-relay-helo + opensmtpd-relay-domain + opensmtpd-relay-host + opensmtpd-relay-pki + opensmtpd-relay-srs + opensmtpd-relay-tls + opensmtpd-relay-auth + opensmtpd-relay-mail-from + opensmtpd-relay-src + + opensmtpd-option + opensmtpd-option? + opensmtpd-option-option + opensmtpd-option-bool + opensmtpd-option-regex + opensmtpd-option-data + + opensmtpd-filter-phase + opensmtpd-filter-phase? + opensmtpd-filter-phase-name + opensmtpd-filter-phase-phase + opensmtpd-filter-phase-options + opensmtpd-filter-phase-decision + opensmtpd-filter-phase-message + opensmtpd-filter-phase-value + + opensmtpd-filter + opensmtpd-filter? + opensmtpd-filter-name + opensmtpd-filter-proc + + opensmtpd-interface + opensmtpd-interface? + opensmtpd-interface-interface + opensmtpd-interface-family + opensmtpd-interface-auth + opensmtpd-interface-auth-optional + opensmtpd-interface-filters + opensmtpd-interface-hostname + opensmtpd-interface-hostnames + opensmtpd-interface-mask-src + opensmtpd-interface-disable-dsn + opensmtpd-interface-pki + opensmtpd-interface-port + opensmtpd-interface-proxy-v2 + opensmtpd-interface-received-auth + opensmtpd-interface-senders + opensmtpd-interface-masquerade + opensmtpd-interface-secure-connection + opensmtpd-interface-tag + + opensmtpd-socket + opensmtpd-socket? + opensmtpd-socket-filters + opensmtpd-socket-mask-src + opensmtpd-socket-tag + + opensmtpd-match + opensmtpd-match? + opensmtpd-match-action + opensmtpd-match-options + + opensmtpd-smtp + opensmtpd-smtp? + opensmtpd-smtp-ciphers + opensmtpd-smtp-limit-max-mails + opensmtpd-smtp-limit-max-rcpt + opensmtpd-smtp-max-message-size + opensmtpd-smtp-sub-addr-delim character + + opensmtpd-srs + opensmtpd-srs? + opensmtpd-srs-key + opensmtpd-srs-backup-key + opensmtpd-srs-ttl-delay + + opensmtpd-queue + opensmtpd-queue? + opensmtpd-queue-compression + opensmtpd-queue-encryption + opensmtpd-queue-ttl-delay + opensmtpd-configuration opensmtpd-configuration? + opensmtpd-package + opensmtpd-config-file + opensmtpd-configuration-bounce + opensmtpd-configuration-cas + opensmtpd-configuration-interfaces + opensmtpd-configuration-socket + opensmtpd-configuration-includes + opensmtpd-configuration-matches + ;;opensmtpd-configuration-mda-wrappers + opensmtpd-configuration-mta-max-deferred + opensmtpd-configuration-srs + opensmtpd-configuration-smtp + opensmtpd-configuration-queue opensmtpd-service-type - %default-opensmtpd-config-file mail-aliases-service-type @@ -1641,22 +1784,2351 @@ (define (generate-dovecot-documentation) (listeners unix-listener-configuration fifo-listener-configuration inet-listener-configuration)) (protocol-configuration ,protocol-configuration-fields)) - 'dovecot-configuration)) + 'dovecot-configuration)) -;;; ;;; OpenSMTPD. ;;; +;;; This next bit of code helps me create my own sanitizer functions. + +;; some fieldnames have a default value of #f, which is ok. They cannot have +;; a value of #t. +;; for example opensmtpd-table-data can be #f, BUT NOT true. +;; my/sanitize procedure tests values to see if they are of the right kind. +;; procedure false? is needed to allow fields like 'values' to be blank, +;; (empty), or #f BUT also have a value like a list of strings. +(define (false? var) + (eq? #f var)) + +;; TODO I have to have this procedure, or I need to change my/sanitize +;; procedure. +(define (my-file-exists? file) + (and (string? file) + (access? file F_OK))) + +;; This procedure takes in a var and a list of procedures. It loops through +;; list of procedures passing in var to each. +;; if one procedure returns #t, the function returns true. Otherwise #f. +;; TODO for fun rewrite this using map +;; If I rewrote it in map, then it may help with sanitizing. +;; eg: I could then potentially easily sanitize vars with lambda procedures. +(define (is-value-right-type? var list-of-procedures record fieldname) + (if (null? list-of-procedures) + #f + (if ((car list-of-procedures) var) + #t + (is-value-right-type? var (cdr list-of-procedures) record + fieldname)))) + +;; converts strings like this: +;; "apple, ham, cherry" -> "apple, ham, or cherry" +;; "pineapple" -> "pinneapple". +;; "cheese, grapefruit, or jam" -> "cheese, grapefruit, or jam" +(define (add-comma-or string) + (define last-comma-location (string-rindex string #\,)) + (if last-comma-location + (if (string-contains string ", or" last-comma-location) + string + (string-replace string ", or" last-comma-location + (+ 1 last-comma-location))) + string)) + + +(define (list-of-procedures->string procedures) + (define string + (let loop ((procedures procedures)) + (if (null? procedures) + "" + (begin + (string-append + (cond ((eq? false? (car procedures)) + "#f, ") + ((eq? boolean? (car procedures)) + "a boolean, ") + ((eq? string? (car procedures)) + "a string, ") + ((eq? integer? (car procedures)) + "an integer, ") + ((eq? list-of-strings? (car procedures)) + "a list of strings, ") + ((eq? assoc-list? (car procedures)) + "an association list of strings, ") + ((eq? nested-list? (car procedures)) + "a nested-list of strings, ") + ((eq? opensmtpd-pki? (car procedures)) + "an record, ") + ((eq? opensmtpd-table? (car procedures)) + "an record, ") + ((eq? list-of-opensmtpd-match? (car procedures)) + "a list of unique records, ") + ((eq? list-of-strings-or-gexps? (car procedures)) + "a list of strings or gexps, ") + ;; TODO can I remove the next two procedures? + ;; tables-data-are-a* ? I think I can. + ((eq? tables-data-are-assoc-list? (car procedures)) + (string-append + "an record whose fieldname 'data' are " + "an assoc-list.\nFor example: (opensmtpd-table " + "(name \"hostnames\") , " + "(data '((\"124.394.23.1\" . \"gnu.org\"))))")) + ((eq? tables-data-are-a-list-of-strings? + (car procedures)) + (string-append + "on record whose fieldname 'data' is " + "a list of strings.\n" + "For example: (opensmtpd-table (name \"domains\") , " + "(data (list \"gnu.org\" \"guix.gnu.org\")))")) + ((eq? my-file-exists? (car procedures)) + "a file, ") + (else "has an incorrect value, ")) + (loop (cdr procedures))))))) + (add-comma-or (string-append (string-drop-right string 2) ".\n"))) + +(define (list-of-strings-or-gexps? list) + (and (list? list) + (cond ((null? list) + #t) + ((or (string? (car list)) + (gexp? (car list)) + (local-file? (car list)) + (file-append? (car list)) + (plain-file? (car list)) + (computed-file? (car list)) + (program-file? (car list))) + (list-of-strings-or-gexps? (cdr list))) + (else #f)))) + +(define (my/sanitize var record fieldname list-of-procedures) + (define try-string + (string-append "Try " (list-of-procedures->string list-of-procedures))) + (if (is-value-right-type? var list-of-procedures record fieldname) + var + (begin + (cond ((string? var) + (report-error (G_ "(~a \"~a\") is invalid.~%") fieldname var)) + ((or (number? var) (boolean? var)) + (report-error (G_ "(~a ~a) is invalid.~%") fieldname var) ) + (else + (report-error (G_ "(~a ...) is invalid.~%Value is: ~a~%") + fieldname var))) + (display-hint (G_ try-string)) + (throw 'bad! var)))) + +;;; The Opensmtpd records. + +;; Some example opensmtpd-tables: +;; +;; (opensmtpd-table (name "root accounts") +;; (data '(("joshua" . "root@dismail.de") +;; ("joshua" . "postmaster@dismail.de")))) +;; (opensmtpd-table (name "root accounts") +;; (data (list "mysite.me" "your-site.com"))) +;; TODO: How am I supporting file: or db: tables? +;; Perhaps I should just automatically convert the table to a database table +;; if the data gets large enough. What would be sufficently large enough? +(define-record-type* + opensmtpd-table make-opensmtpd-table + opensmtpd-table? + (name opensmtpd-table-name ;; string + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-table" "name" + (list string?))))) + ;; FIXME Support an aliasing table as described here: + ;; https://man.openbsd.org/table.5 + ;; One may have to use the record file for this. I don't think tables + ;; support a table like this: + ;; table "name" { joshua = joshua@gnucode.me,joshua@gnu-hurd.com, \ + ;; joshua@propernaming.org, root = root@gnucode.me } + ;; If values is an absolute filename, then it will use said filename to + ;; house the table info. filename must be an absolute filename. + (data opensmtpd-table-data + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-table" "data" + (list list-of-strings? assoc-list? + nested-list?)))))) + +(define-record-type* + opensmtpd-ca make-opensmtpd-ca + opensmtpd-ca? + (name opensmtpd-ca-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-ca" "name" (list string?))))) + (file opensmtpd-ca-file + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-ca" "file" + (list my-file-exists?)))))) + +(define-record-type* + opensmtpd-pki make-opensmtpd-pki + opensmtpd-pki? + (domain opensmtpd-pki-domain + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-pki" "domain" + (list string?))))) + ;; TODO/FIXME this should probably be a list of files. The opensmtpd + ;; documentation says that you could have a list of files: + ;; + ;; pki pkiname cert certfile + ;; Associate certificate file certfile with host pkiname, and use that file + ;; to prove the identity of the mail server to clients. pkiname is the + ;; server's name, derived from the default hostname or set using either + ;; /gnu/store/2d13sdz76ldq8zgwv4wif0zx7hkr3mh2-opensmtpd-6.8.0p2/etc/mailname + ;; or using the hostname directive. If a fallback certificate or SNI is + ;; wanted, the ‘*’ wildcard may be used as pkiname. + + ;; A certificate chain may be created by appending one or many certificates, + ;; including a Certificate Authority certificate, to certfile. The creation + ;; of certificates is documented in starttls(8). + (cert opensmtpd-pki-cert + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-pki" "cert" + (list my-file-exists?))))) + (key opensmtpd-pki-key + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-pki" "key" + (list my-file-exists?))))) + ; todo sanitize this. valid parameters are "none", "legacy", or "auto". + (dhe opensmtpd-pki-dhe + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-dhe" "dhe" + (list false? string?)))))) + +(define-record-type* + opensmtpd-lmtp make-opensmtpd-lmtp + opensmtpd-lmtp? + (destination opensmtpd-lmtp-destination + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-lmtp" "destination" + (list string?))))) + (rcpt-to opensmtpd-lmtp-rcpt-to + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-lmtp" "rcpt-to" + (list false? string?)))))) + +(define-record-type* + opensmtpd-mda make-opensmtpd-mda + opensmtpd-mda? + (name opensmtpd-mda-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-mda" "name" + (list string?))))) + ;; TODO should I allow this command to be a gexp? + (command opensmtpd-mda-command + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-mda" "command" + (list string?)))))) + +(define-record-type* + opensmtpd-maildir make-opensmtpd-maildir + opensmtpd-maildir? + (pathname opensmtpd-maildir-pathname + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-maildir" "pathname" + (list false? string?))))) + (junk opensmtpd-maildir-junk + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-maildir" "junk" + (list boolean?)))))) + +(define-record-type* + opensmtpd-local-delivery make-opensmtpd-local-delivery + opensmtpd-local-delivery? + (name opensmtpd-local-delivery-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "name" + (list string?))))) + (method opensmtpd-local-delivery-method + (default "mbox") + (sanitize (lambda (var) + (define fieldname "method") + (define options (list "mbox" "expand-only" + "forward-only")) + (define options-plus-records + (append options (list "(opensmtpd-lmtp ...)" + "(opensmtpd-maildir ...)" + "(opensmtpd-mda ...)"))) + (cond ((or (opensmtpd-lmtp? var) + (opensmtpd-maildir? var) + (opensmtpd-mda? var) + (member var options)) + var) + (else + (begin + (report-error (G_ "(~a \"~a\") is invalid.~%") + fieldname var) + (display-hint + (G_ (hint-string + var + options-plus-records + #:fieldname fieldname))) + (throw 'bad! var))))))) + (alias opensmtpd-local-delivery-alias + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "alias" + (list false? + tables-data-are-assoc-list?))))) + (ttl opensmtpd-local-delivery-ttl + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "ttl" + (list false? string?))))) + (user opensmtpd-local-delivery-user + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "user" + (list false? string?))))) + (userbase opensmtpd-local-delivery-userbase + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "userbase" + (list false? + tables-data-are-assoc-list?))))) + (virtual opensmtpd-local-delivery-virtual + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "virtual" + (list + false? + tables-data-are-assoc-list?))))) + (wrapper opensmtpd-local-delivery-wrapper + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-local-delivery" "wrapper" + (list false? string?)))))) + +(define-record-type* + opensmtpd-relay make-opensmtpd-relay + opensmtpd-relay? + (name opensmtpd-relay-name + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "name" + (list string?)))) + (default #f)) + (backup opensmtpd-relay-backup ;; boolean + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "backup" + (list boolean?))))) + (backup-mx opensmtpd-relay-backup-mx ;; string mx name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "backup-mx" + (list false? string?))))) + (helo opensmtpd-relay-helo + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "helo" + (list false? string?)))) + (default #f)) + (helo-src opensmtpd-relay-helo-src + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "helo-src" + (list false? string? + tables-data-are-assoc-list?)))) + (default #f)) + (domain opensmtpd-relay-domain + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "domain" + (list false? opensmtpd-table?)))) + (default #f)) + (host opensmtpd-relay-host + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "host" + (list false? string?)))) + (default #f)) + (pki opensmtpd-relay-pki + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "pki" + (list false? opensmtpd-pki?))))) + (srs opensmtpd-relay-srs + (default #f) + (lambda (var) + (my/sanitize var "opensmtpd-relay" "srs" + (list boolean?)))) + (tls opensmtpd-relay-tls + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "tls" + (list false? string?))))) + ;; the table here looks like: + ;; label1 user:password + ;; label2 user2:password2 + ;; It is documented in the credentials table in man table + (auth opensmtpd-relay-auth + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "auth" + (list false? + tables-data-are-assoc-list?)))) + (default #f)) + (mail-from opensmtpd-relay-mail-from + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-relay" "mail-from" + (list false? string?)))) + (default #f)) + ;; string "127.0.0.1" or "" or "
" + ;; TODO should I do some sanitizing to make sure that the string? + ;; here is actually an IP address or a valid interface? + (src opensmtpd-relay-src + (sanitize + (lambda (var) + (my/sanitize var "opensmtpd-relay" "src" + (list false? string? + tables-data-are-a-list-of-strings?)))) + (default #f))) + +;; this record is used by & +;; +(define-record-type* + opensmtpd-option make-opensmtpd-option + opensmtpd-option? + (option opensmtpd-option-option + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-option" "option" + (list string?))))) + (bool opensmtpd-option-bool + (default #t) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-option" "not" + (list boolean?))))) + (regex opensmtpd-option-regex + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-option" "regex" + (list boolean?))))) + (data opensmtpd-option-data + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-option" "data" + (list false? string? opensmtpd-table?)))))) + +(define-record-type* + opensmtpd-filter-phase make-opensmtpd-filter-phase + opensmtpd-filter-phase? + (name opensmtpd-filter-phase-name ;; string + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter-phase" "name" + (list string?))))) + (phase opensmtpd-filter-phase-phase ;; string + (default #f) + (sanitize + (lambda (var) + (define options + (list "connect" "helo" "ehlo" "mail-from" + "rcpt-to" "data" "commit")) + (define fieldname "phase") + (if (and (string? var) + (member var options)) + var + (begin + (report-error + (G_ + "(opensmtpd-filter-phase ... (~a \"~a\")) is invalid.~%") + fieldname var) + (display-hint + (G_ (hint-string var options + #:fieldname fieldname))) + (throw 'bad! var)))))) + (options opensmtpd-filter-phase-options + (default #f) + (sanitize + (lambda (var) + (cond + ((false? var) + (report-error (G_ "")) + (display "(opensmtpd-filter-phase (options #f)) is invalid.\n") + (display-hint + (G_ "Try a list of (opensmtpd-option) records.\n")) + (throw 'bad! #f)) + ((not (list-of-opensmtpd-option? var)) + (report-error (G_ "")) + (display "(opensmtpd-filter-phase (options ...) is invalid.\n") + (display-hint + (G_ "Try a list of (opensmtpd-option) records.\n")) + (throw 'bad! var)) + (else (sanitize-options-for-filter-phase var)))))) + (decision opensmtpd-filter-phase-decision + (default #f) + (sanitize + (lambda (var) + (define options + (list "bypass" "disconnect" + "reject" "rewrite" "junk")) + (define fieldname "decision") + (if (and (string? var) + (member var options)) + var + (begin + (report-error (G_ "(~a \"~a\") is invalid.~%") + fieldname var) + (display-hint (G_ (hint-string var options + #:fieldname fieldname))) + (throw 'bad! var)))))) + (message opensmtpd-filter-phase-message + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter-phase" "message" + (list false? string?))))) + (value opensmtpd-filter-phase-value + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter-phase" "value" + (list false? number?)))))) + +(define-record-type* + opensmtpd-filter make-opensmtpd-filter + opensmtpd-filter? + (name opensmtpd-filter-name + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter" "name" + (list string?))))) + (exec opensmtpd-filter-exec + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter" "exec" + (list boolean?))))) + ;; a string like "rspamd" or the command to start it like + ;; "/path/to/rspamd --option=arg --2nd-option=arg2" + ;; OR a list of strings and/or geps. + (proc opensmtpd-filter-proc + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-filter" "proc" + (list string? + list-of-strings-or-gexps?)))))) + +;; There is another type of filter that opensmtpd supports, which is a +;; filter chain. A filter chain is a list of s +;; and/or s. This lets you apply several filters under +;; one filter name. I could have defined a record type for it, but the +;; record would only have had two fields: name and list-of-filters. +;; Why write that as a record? It makes the user of this service harder. +;; Instead, just define it as a list, and if a user wants an interface +;; to make multiple filters, he just appends to the 'filters' fieldname. +;; +;; returns #t if list is a unique list of or +;; +;; returns # otherwise +(define (opensmtpd-filter-chain? %filters) + (and (list-of-unique-filter-or-filter-phase? %filters) + (< 1 (length %filters)))) + +(define-record-type* + opensmtpd-interface make-opensmtpd-interface + opensmtpd-interface? + ;; interface may be an IP address, interface group, or domain name + (interface opensmtpd-interface-interface + (default "lo") + (sanitize (lambda (var) + (my/sanitize var "interface" "interface" + (list string?))))) + (family opensmtpd-interface-family + (default #f) + (sanitize + (lambda (var) + (define options (list "inet4" "inet6")) + (define fieldname "family") + (cond + ((eq? #f var) ;; var == #f + var) + ((and (string? var) + (member var options)) + var) + (else + (begin + (report-error (G_ "(~a \"~a\") is invalid.~%") fieldname var) + (display-hint (G_ (hint-string var options + #:fieldname fieldname))) + (throw 'bad! var))))))) + (auth opensmtpd-interface-auth + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "auth" + (list boolean? + tables-data-are-assoc-list?))))) + (auth-optional opensmtpd-interface-auth-optional + (default #f) + (sanitize + (lambda (var) + (my/sanitize var "opensmtpd-interface" "auth-optional" + (list boolean? + tables-data-are-assoc-list?))))) + ;; TODO add a ca entry? + ;; string FIXME/TODO sanitize this to support a gexp. That way way the + ;; includes directive can include my hacky scheme code that I use + ;; for opensmtpd-dkimsign. + (filters opensmtpd-interface-filters + (default #f) + (sanitize (lambda (var) + (sanitize-socket-and-interfaces-filters var)))) + (hostname opensmtpd-interface-hostname + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "hostname" + (list false? string?))))) + (hostnames opensmtpd-interface-hostnames + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "hostnames" + (list + false? + tables-data-are-assoc-list?))))) + (mask-src opensmtpd-interface-mask-src + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "mask-src" + (list boolean?))))) + (disable-dsn opensmtpd-interface-disable-dsn + (default #f)) + (pki opensmtpd-interface-pki + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "pki" + (list false? opensmtpd-pki?))))) + (port opensmtpd-interface-port + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "port" + (list false? integer?))))) + (proxy-v2 opensmtpd-interface-proxy-k2 + (default #f)) + (received-auth opensmtpd-interface-received-auth + (default #f)) + (senders opensmtpd-interface-senders + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "senders" + (list false? + tables-data-are-assoc-list?)))) + (default #f)) + (masquerade opensmtpd-interface-masquerade + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "masquerade" + (list boolean?)))) + (default #f)) + (secure-connection opensmtpd-interface-secure-connection + (default #f) + (sanitize + (lambda (var) + (define options + (list "smtps" "tls" "tls-require" + "tls-require-verify")) + (define fieldname "secure-connection") + (cond ((boolean? var) + var) + ((and (string? var) + (member var options)) + var) + (else + (begin + (report-error + (G_ "(~a \"~a\") is invalid.~%") + fieldname var) + (display-hint + (G_ (hint-string var options + #:fieldname fieldname))) + (throw 'bad! var))))))) + (tag opensmtpd-interface-tag + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "tag" + (list false? string?)))) + (default #f))) + +(define-record-type* + opensmtpd-socket make-opensmtpd-socket + opensmtpd-socket? + ;; false or or list of + (filters opensmtpd-socket-filters + (sanitize (lambda (var) + (sanitize-socket-and-interfaces-filters + var + #:socket-or-interface "socket"))) + (default #f)) + (mask-src opensmtpd-socket-mask-src + (default #f) + (my/sanitize var "opensmtpd-interface" "mask-src" + (list false? boolean?))) + (tag opensmtpd-socket-tag + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-interface" "tag" + (list false? string?)))) + (default #f))) + + +(define-record-type* + opensmtpd-match make-opensmtpd-match + opensmtpd-match? + ;;TODO? Perhaps I should add in a reject fieldname. If reject + ;;is #t, then the match record will be a reject match record. + ;; (opensmtpd-match (reject #t)) vs. (opensmtpd-match (action 'reject)) + ;; To do this, I will also have to 'reject' mutually exclusive. + ;; AND an match with 'reject' can have no action defined. + (action opensmtpd-match-action + (default #f) + (sanitize + (lambda (var) + (define fieldname "action") + (if (or (opensmtpd-relay? var) + (opensmtpd-local-delivery? var) + (eq? (quote reject) var)) + var + (begin + (report-error (G_ "(~a \"~a\") is invalid.~%") + fieldname var) + (display-hint + (G_ "Try an (opensmtpd-relay) record, +(opensmtpd-local-delivery) record, or (quote reject).")) + (throw 'bad! var)))))) + (options opensmtpd-match-options + (default #f) + (sanitize (lambda (var) + (sanitize-options-for-opensmtpd-match var))))) + +(define-record-type* + opensmtpd-smtp make-opensmtpd-smtp + opensmtpd-smtp? + (ciphers opensmtpd-smtp-ciphers + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp" "ciphers" + (list false? string?))))) + (limit-max-mails opensmtpd-smtp-limit-max-mails + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp" + "limit-max-mails" + (list false? integer?))))) + (limit-max-rcpt opensmtpd-smtp-limit-max-rcpt + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp" + "limit-max-rcpt" + (list false? integer?))))) + ;; TODO the user could enter in "zebra" which would break the config. + ;; I should sanitize the string to make sure it looks like "50M". + (max-message-size opensmtpd-smtp-max-message-size + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp" + "max-message-size" + (list false? integer? + string?))))) + ;; FIXME/TODO the sanitize function of sub-addr-delim should accept a + ;; string of length one not string? + (sub-addr-delim opensmtpd-smtp-sub-addr-delim + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-smtp" + "sub-addr-delim" + (list false? integer? string?)))))) + +(define-record-type* + opensmtpd-srs make-opensmtpd-srs + opensmtpd-srs? + (key opensmtpd-srs-key + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-srs" "key" + (list false? boolean? my-file-exists?))))) + (backup-key opensmtpd-srs-backup-key + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-srs" "backup-key" + (list false? integer? + my-file-exists?))))) + ;; TODO the user could set the string to be "zebra", which would break + ;; the config. + (ttl-delay opensmtpd-srs-ttl-delay + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-srs" "ttl-delay" + (list false? string?)))))) + +(define-record-type* + opensmtpd-queue make-opensmtpd-queue + opensmtpd-queue? + (compression opensmtpd-queue-compression + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-queue" "compression" + (list boolean?))))) + (encryption opensmtpd-queue-encryption + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-queue" "encryption" + (list boolean? string? + my-file-exists?))))) + ;; TODO the user could set the string to be "zebra", which would break + ;; the config. + (ttl-delay opensmtpd-queue-ttl-delay + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-queue" "ttl-delay" + (list false? string?)))))) (define-record-type* opensmtpd-configuration make-opensmtpd-configuration opensmtpd-configuration? - (package opensmtpd-configuration-package - (default opensmtpd)) + (package opensmtpd-configuration-package + (default opensmtpd)) (config-file opensmtpd-configuration-config-file - (default %default-opensmtpd-config-file)) + (default #f)) + ;; FIXME/TODO should I include a admd authservid entry? + (bounce opensmtpd-configuration-bounce + (default #f) + (sanitize + (lambda (var) + (cond ((false? var) + var) + ((and (list? var) + (>= 4 (length var)) + (<= 1 (length var)) + (list-of-strings? var) + (every (lambda (str) + (and (<= 2 (string-length str)) + ;; last character of str is 's' or 'm' + ;; or 'h' or 'd'. + (member (string-take-right str 1) + (list "s" "m" "h" "d")) + ;; first part of str is an integer. + (integer? + (string->number + (string-take str + (- (string-length str) + 1 )))))) + var)) + var) + (else + ;; FIXME TODO I am getting a warning that says + ;; possibly wrong number of arguments to `G_' + ;; is one of the below lines to blame? + (if (string? var) + (report-error (G_ "(bounce \"~a\") is invalid.\n") var) + (report-error (G_ "(bounce ~a) is invalid.\n") var)) + (display-hint (G_ "Try (bounce (list \"30m\" \"2h\"))\n")) + (throw 'bad! var)))))) + (cas opensmtpd-configuration-cas + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "cas" + (list false? list-of-opensmtpd-ca?))))) + + ;; list of many records of type opensmtpd-interface + (interfaces opensmtpd-configuration-interfaces + (default (list (opensmtpd-interface))) + (sanitize + (lambda (var) + ;; This makes sure that no opensmtpd-interface is like this: + ;; (opensmtpd-interface (senders #f) (masquerade #t)), which + ;; is a syntax error. + (define (correct-senders? interface) + (not + (and (not (opensmtpd-interface-senders interface)) + (opensmtpd-interface-masquerade interface)))) + + (define fieldname "interface") + + ;; TODO rework this sanitize bit, so that if someone writes: + ;; (opensmtpd-interface (senders #f) (masquerade #t)), they + ;; get a proper error. + ;; (report-error + ;; (G_ "((senders #f) & (masquerade #t)) is invalid.\n")) + (if (and (list-of-interface? var) + (every correct-senders? var) + (not (contains-duplicate? var))) + var + (begin + (display " fieldname ") + (display "'interface' may be #f or a list of records") + (display "\n of unique .\n") + (throw 'bad! var)))))) + (socket opensmtpd-configuration-socket + (default #f) + (sanitize + (lambda (var) + (define fieldname "socket") + (if (or (opensmtpd-socket? var) + (false? var)) + var + (begin + (report-error (G_ "(~a \"~a\") is invalid.~%") + fieldname var) + (display-hint + (G_ + (string-append "Try an (" + fieldname + " (opensmtpd-socket ...)) .\n"))) + (throw 'bad! var)))))) + ;; list of strings of absolute path names + (includes opensmtpd-configuration-includes + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "includes" + (list false? list-of-strings? gexp?))))) + (matches opensmtpd-configuration-matches + (default (list (opensmtpd-match + (action (opensmtpd-local-delivery + (name "local") + (method "mbox"))) + (options (list + (opensmtpd-option + (option "for local"))))) + (opensmtpd-match + (action (opensmtpd-relay + (name "outbound"))) + (options (list + (opensmtpd-option + (option "from local")) + (opensmtpd-option + (option "for any"))))))) + ;; TODO perhaps I should sanitize this function like I sanitized + ;; the 'filters'. For example, you could have two different. + ;; actions, one for local delivery and one for remote, + ;; with the same name. + ;; It might be a bit complicated to do this. + ;; I might just let smtpd figure out if the user made a silly + ;; mistake by having two different actions with the same name. + (sanitize (lambda (var) + var + (my/sanitize var "opensmtpd-configuration" "matches" + (list list-of-opensmtpd-match?))))) + ;; list of many records of type mda-wrapper + ;; TODO/FIXME support using gexps here + ;; eg (list "name" gexp) + ;; TODO what are mda-wrappers for? How do I use this fieldname? + ;; (mda-wrappers opensmtpd-configuration-mda-wrappers + ;; (default #f) + ;; (sanitize (lambda (var) + ;; (my/sanitize var + ;; "opensmtpd-configuration" + ;; "mda-wrappers" + ;; (list false? string?))))) + (mta-max-deferred opensmtpd-configuration-mta-max-deferred + (default 100) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" + "mta-max-deferred" + (list number?))))) + (queue opensmtpd-configuration-queue + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "queue" + (list false? opensmtpd-queue?))))) + (smtp opensmtpd-configuration-smtp + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "smtp" + (list false? opensmtpd-smtp?))))) + (srs opensmtpd-configuration-srs + (default #f) + (sanitize (lambda (var) + (my/sanitize var "opensmtpd-configuration" "srs" + (list false? opensmtpd-srs?))))) (setgid-commands? opensmtpd-setgid-commands? (default #t))) +;; this help procedure is used 3 or 4 times by +;; sanitize-options-for-opensmtpd-match +(define* (throw-error-duplicate-option option error-arg + #:key (record-name "match")) + (throw-error error-arg + (list + (string-append "(opensmtpd-" record-name + ") (options ...)) can only have one \n" + "(opensmtpd-option (option \"" option + "\")) record, but more are present. \n")) + #:duplicate-option #t)) + +;; this procedure sanitizes the fieldname opensmtpd-match-options +(define* (sanitize-options-for-opensmtpd-match %options) + (define option-list (list "for any" "for local" "for domain" + "for rcpt-to" "from any" "from auth" + "from local" "from mail-from" "from rdns" + "from socket" "from src" "auth" "helo" + "mail-from" "rcpt-to" "tag" "tls")) + (when (not (list-of-opensmtpd-option? %options)) + (report-error (G_ "")) + (display + (string-append "(opensmtpd-match (options ...)) is a list of unique" + " (opensmtpd-option ...) records.\nIt's value is: ")) + (display %options) + (display "\n") + (throw 'bad! %options)) + + (let loop ((%traversing-options %options) + ;; sanitized-options is an alist that may end of looking + ;; like: + ;; (("for" (opensmtpd-option (option "for any"))) + ;; ("from" (opensmtpd-option (option "from any"))) + ;; ("tag (opensmtpd-option (option "tag") (data "tag"))) + (%sanitized-options '()) + (option-record (if (null? %options) + '() + (car %options))) + (option-string (if (null? %options) + '() + (opensmtpd-option-option + (car %options))))) + + (cond + ((null? %traversing-options) + %options) + ;; error if option-string is invalid option + ((not (member option-string option-list)) + (report-error (G_ "(opensmtpd-match \"~a\")) is invalid.\n") + option-string) + (display-hint (G_ "")) + (display (hint-string option-string option-list #:fieldname "option")) + (throw 'bad! option-string)) + ;; error, if duplicate option + ((assoc-ref %sanitized-options option-string) + (report-error (G_ "")) + (display (string-append "(opensmtpd-match (options ...)) can " + "only have one (opensmtpd-option (option " + "\"" option-string "\")), but more " + " \n are present.\n")) + (display-hint + (format #f (G_ "Try removing one (opensmtpd-option (option \"~a\")).~%") + option-string)) + (throw 'bad! option-record)) + ;; error, if duplicate from or duplicate for option + ((or + (if (and (string=? "for" (substring option-string 0 3)) + (assoc-ref %sanitized-options "for")) + #t + #f) + (if (and (>= (length (string->list option-string)) 4) + (string=? "from" (substring option-string 0 4)) + (assoc-ref %sanitized-options "from")) + #t + #f)) + (throw-error %options + (list "(opensmtpd-match (options ...)) can only have one" + " (option \"for ...\") and one (option \"from ...\")" + "\nBut (option \"" option-string "\") and (option \"" + (opensmtpd-option-option + (if (assoc-ref %sanitized-options "for") + (assoc-ref %sanitized-options "for") + (assoc-ref %sanitized-options "from"))) + "\") are present.\n") + #:hint-strings + (list "Try removing one " + (if (string=? "for" (substring option-string 0 3)) + "(opensmtpd-option (option \"for ...\"))" + "(opensmtpd-option (option \"from ...\"))") + " record.\n"))) + ;; these 3 options must have fieldname data defined. + ((and (member option-string + (list "helo" "mail-from" "rcpt-to")) + (not (opensmtpd-option-data option-record))) + (report-error (G_ "")) + (display (string-append "(option \"" option-string + "\") must have (data ...) of type string or an " + "(opensmtpd-table ...) record.\n")) + (throw 'bad! option-string)) + ;; fieldname data must be a string. + ((and (string=? "tag" option-string) + (not (string? (opensmtpd-option-data option-record)))) + (throw-error option-record + (list "(opensmtpd-match ... (option \"tag\"))" + " must have a 'data' of type string.\n"))) + ((or (string=? "tls" option-string) + (string=? "for" (substring option-string 0 3)) + (string=? "from" (substring option-string 0 4))) + ;; let's test the "for" and "from" options now. + (cond + ;; the options in this list cannot define 'data' or 'regex' + ;; fieldnames. + ((and (member option-string (list "for local" "for any" + "from any" "from local" + "from socket" "tls")) + (or (opensmtpd-option-data option-record) + (opensmtpd-option-regex option-record))) + (report-error (G_ "")) + (display (string-append "When (openmstpd-option (option \"" + option-string "\") ...), " + "then (data ...) and (regex ...) " + "must be #f. \n")) + (throw 'bad! option-record)) + ;; the options in this list must have a data field of type + ;; string or tables-data-are-a-list-of-strings? + ((and (member option-string + (list "for domain" "for rcpt-to" + "from mail-from" "from src")) + (or (false? (opensmtpd-option-data option-record)) + (tables-data-are-assoc-list? + (opensmtpd-option-data option-record)))) + (throw-error option-record + (list "When (openmstpd-option (option \"" + option-string "\") ...) \n" + "then (data ...) must be a string or an \n" + "(opensmtpd-table ....) record whose " + "'data' is a list of strings.\n"))) + (else + (loop (cdr %traversing-options) + (alist-cons + (cond ((string=? "for" (substring option-string 0 3)) + "for") + ((string=? "tls" option-string) + "tls") + (else "from")) + option-record + %sanitized-options) + ;;option-record + (if (null? (cdr %traversing-options)) + '() + (car (cdr %traversing-options))) + ;; option-string + (if (null? (cdr %traversing-options)) + '() + (opensmtpd-option-option + (car (cdr %traversing-options)))))))) + ;; TODO if auth's 'data' is an assoc-list table, then + ;; it IS invalid! + ;; option-string = 'auth' cannot be made invalidly, + ;; do not test for it. + (else + (loop (cdr %traversing-options) + (alist-cons option-string option-record + %sanitized-options) + ;;option-record + (if (null? (cdr %traversing-options)) + '() + (car (cdr %traversing-options))) + ;; option-string + (if (null? (cdr %traversing-options)) + '() + (opensmtpd-option-option + (car (cdr %traversing-options))))))))) + +(define (filter-phase-has-message-and-value? record) + (and (opensmtpd-filter-phase-message record) + (opensmtpd-filter-phase-value record))) + +;; return #t if phase needs a message. Or if the message did not start +;; with a 4xx or 5xx status code. otherwise #f +(define (filter-phase-decision-lacks-proper-message? record) + (define decision (opensmtpd-filter-phase-decision record)) + (if (member decision (list "disconnect" "reject")) + ;; this message needs to be RFC compliant, meaning + ;; that it need to start with 4xx or 5xx status code + (cond ((eq? #f (opensmtpd-filter-phase-message record)) + #t) + ((string? (opensmtpd-filter-phase-message record)) + (let ((number (string->number + (substring + (opensmtpd-filter-phase-message record) 0 3)))) + (if (and (number? number) + (and (< number 600) (> number 399))) + #f + #t)))) + #f)) + +;; 'decision' "rewrite" requires 'value' to be a number. +(define (filter-phase-lacks-proper-value? record) + (define decision (opensmtpd-filter-phase-decision record)) + (if (string=? "rewrite" decision) + (if (and (number? (opensmtpd-filter-phase-value record)) + (eq? #f (opensmtpd-filter-phase-message record))) + #f + #t) + #f)) + +;; 'decision' "junk" or "bypass" cannot have a message or a value. +(define (filter-phase-has-incorrect-junk-or-bypass? record) + (and + (member + (opensmtpd-filter-phase-decision record) + (list "junk" "bypass")) + (or + (opensmtpd-filter-phase-value record) + (opensmtpd-filter-phase-message record)))) + +(define (filter-phase-junks-after-commit? record) + (and (string=? "junk" (opensmtpd-filter-phase-decision record)) + (string=? "commit" (opensmtpd-filter-phase-phase record)))) + +;; returns #t if list is a unique list of or +;; returns # otherwise +;; only opensmtpd-filter-chain? uses this function, and opensmtpd-filter-chain +;; is NEVER actually used. +;; I could possibly remove it. +(define (list-of-unique-filter-or-filter-phase? %filters) + (and (list? %filters) + (not (null? %filters)) + ;; this list is made up of only + ;; or + (every (lambda (filter) + (or (opensmtpd-filter? filter) + (opensmtpd-filter-phase? filter))) + %filters) + ;; each filter-name is unique. + (not (duplicate-filter-name %filters)))) + +(define (filters->list-of-filter-names %filters) + (map (lambda (filter) + (cond ((opensmtpd-filter-phase? filter) + (opensmtpd-filter-phase-name filter)) + (else (opensmtpd-filter-name filter)))) + %filters)) + +(define (duplicate-string-in-list strings) + (define first-string (car strings)) + (cond ((null? (cdr strings)) + #f) + ((any (lambda (element) + (if (string=? element first-string) + element + #f)) + (cdr strings)) + first-string) + (else (duplicate-string-in-list (cdr strings))))) + +(define (duplicate-filter-name %filters) + (define filter-names (filters->list-of-filter-names %filters)) + (duplicate-string-in-list filter-names)) + +;; the sanitize procedures used for sanitizing each and +;; fieldname 'filters'. +;; It primarily sanitizes . The only sanitization it does +;; for s, is no make sure there are no duplicate filter names. + +(define* (sanitize-socket-and-interfaces-filters + %filters + #:key (socket-or-interface "interface")) + + ;; if there are two filters with the same name, store that name here. + (define the-duplicate-filter-name + (if (not %filters) + #f + (duplicate-filter-name %filters))) + + (define %filter-phases + (if (not %filters) + '() + (remove opensmtpd-filter? %filters))) + ;; the order of the first two tests in this cond is important. + ;; (false?) has to be 1st and (duplicate-filter-filter-name) has to be + ;; second. You may optionally re-order the other alternates in the cond. + (cond ((false? %filters) + #f) + (the-duplicate-filter-name + (report-error (G_ "")) + (display (string-append + "(opensmtpd-" socket-or-interface + " (filters ...)) has a duplicate filter name: \"" + the-duplicate-filter-name "\".\n")) + (throw 'bad! %filters)) + (else + (let loop ((%traversing-list %filter-phases) + (fieldname (if (null? %filter-phases) + '() + (opensmtpd-filter-phase-decision + (car %filter-phases))))) + (cond + ((null? %traversing-list) + %filters) + ((opensmtpd-filter? (car %traversing-list)) + (loop (cdr %traversing-list) + (if (null? (cdr %traversing-list)) + '() + (opensmtpd-filter-phase-decision + (car (cdr %traversing-list)))))) + ((filter-phase-has-message-and-value? + (car %traversing-list)) + (report-error (G_ "")) + (display + (string-append "(opensmtpd-filter-phase ...) cannot define " + "fieldnames 'value' \n and 'message'.\n"))) + ((filter-phase-decision-lacks-proper-message? + (car %traversing-list)) + (cond + ((string? fieldname) + (report-error + (G_ "(decision \"~a\") with (message ...) is invalid.~%") + fieldname)) + ((or (integer? fieldname) (boolean? fieldname)) + (report-error + (G_ "(decision ~a) with (message ...) is invalid.~%") + fieldname)) + (else + (report-error + (G_ "(~a ...\") with (message ...) is invalid.~%... is ~a") + fieldname))) + (display-hint + (G_ (string-append "Try (opensmtpd-filter-phase " + "(message \"406 Not acceptable.\") " + "(decision \"" fieldname "\")).\n"))) + (throw 'bad! (car %traversing-list))) + ((filter-phase-lacks-proper-value? (car %traversing-list)) + (begin + (report-error (G_ "")) + (display + (string-append + "(opensmtpd-filter-phase (decision \"rewrite\")" + "\n\t\t(value ...)) must be a number.\n")) + (display-hint (G_ "Try (value 5).")) + (throw 'bad! (car %traversing-list)))) + ((filter-phase-has-incorrect-junk-or-bypass? + (car %traversing-list)) + (begin + (report-error (G_ "")) + (display + (string-append "(opensmtpd-filter-phase (decision \"" + (opensmtpd-filter-phase-decision + (car %traversing-list)) + "\") cannot define (message ...) or " + "(value ...).\n")) + (throw 'bad! (car %traversing-list)))) + ((filter-phase-junks-after-commit? (car %traversing-list)) + (begin + (report-error (G_ "")) + (display + (string-append + "(opensmtpd-filter-phase (decision \"junk\")\n\t\t " + "(phase \"commit\")) is invalid.\n")) + (display-hint + (G_ (string-append "You cannot junk an email during phase " + "\"commit\". Try (phase \"data\").\n"))) + (throw 'bad! (car %traversing-list)))) + (else (loop (cdr %traversing-list) + (if (null? (cdr %traversing-list)) + '() + (opensmtpd-filter-phase-decision + (car (cdr %traversing-list))))))))))) + +(define* (sanitize-options-for-filter-phase %options) + (define option-list + (list "fcrdns" "rdns" "src" "helo" "auth" "mail-from" "rcpt-to")) + (let loop ((%traversing-options %options) + ;; sanitized-options is an alist that may end of looking like: + ;; (("fcrdns" (opensmtpd-option (option "fcrdns"))) + ;; ("auth" (opensmtpd-option (option "auth")))) + (%sanitized-options '()) + (option-record (if (null? %options) + '() + (car %options))) + (option-string (if (null? %options) + '() + (opensmtpd-option-option (car %options))))) + (cond + ((null? %traversing-options) + %options) + ;; error if option-string is invalid option + ((not (member option-string option-list)) + (report-error + (G_ "(opensmtpd-filter-phase (option \"~a\")) is invalid.\n") + option-string) + (display-hint (G_ "")) + (display (hint-string option-string option-list + #:fieldname "option")) + (throw 'bad! option-string)) + + ;; if we see two "rdns" (for example), throw a + ;; "duplicate option" error. + ((assoc-ref %sanitized-options option-string) + (report-error (G_ "")) + (display (string-append "(opensmtpd-filter-phase (options ...)) can " + "only have one\n (opensmtpd-option (option \"" + option-string "\")), but more are present.\n")) + (display-hint + (format #f (G_ "Try removing one (option \"~a\").~%") option-string)) + (throw 'bad! option-record)) + + ;; the next 4 options must have fieldname 'data' defined. + ((and (member option-string + (list "src" "helo" "mail-from" "rcpt-to")) + (not (opensmtpd-table? (opensmtpd-option-data option-record)))) + (report-error (G_ "")) + (display (string-append "(opensmtpd-filter-phase ... " "(option \"" + option-string "\")) must define (data ...).\n")) + (display-hint (G_ "Try defining (data (opensmtpd-table ...).\n")) + (throw 'bad! option-record)) + ;;fcrdns cannot have fieldname data defined + ((and (string=? "fcrdns" option-string) + (opensmtpd-option-data option-record)) + (report-error (G_ "")) + (display (string-append "(opensmtpd-option \"" option-string "\") " + "cannot define (data ...).\n")) + (display-hint (G_ "")) + (display "Try removing (data ...).\n") + (throw 'bad! option-record)) + ;; rdns and auth cannot be made invalidly. + ;; skip testing them. + (else (loop (cdr %traversing-options) + (alist-cons option-string option-record + %sanitized-options) + ;; option-record + (if (null? (cdr %traversing-options)) + '() + (car (cdr %traversing-options))) + ;; option-string + (if (null? (cdr %traversing-options)) + '() + (opensmtpd-option-option + (car (cdr %traversing-options))))))))) + +(define* (throw-error var %strings + #:key + (record-name #f) + (duplicate-option #f) + (fieldname #f) + (hint-strings #f)) + (cond ((and record-name fieldname) + (cond ((or (string? var)) + (report-error (G_ "(~a \"~a\") is invalid.~%") fieldname var)) + ((boolean? var) + (report-error (G_ "(~a ~a) is invalid.~%") fieldname var)) + ((number? var) + (report-error (G_ "(~a ~a) is invalid.~%") fieldname + (number->string var))) + (else + (report-error (G_ "(~a ...) is invalid.~%Its value is: ~a~%") + fieldname var))) + (display-hint (G_ (string-append "(opensmtpd-" record-name + " (fieldname " fieldname "...)) " + (apply string-append %strings)))) + (throw 'bad! var)) + ((list? hint-strings) + (report-error (G_ "")) + (display (apply string-append %strings)) + (display-hint (G_ (apply string-append hint-strings))) + (throw 'bad! var)) + ;; display the output for throw-error-duplicate-option + (duplicate-option + (report-error (G_ "")) + (display (apply string-append %strings)) + (display-hint + (format #f + (G_ "Try removing one (opensmtpd-option \"~a\") option.\n") + var)) + (throw 'bad! var)) + (else + (report-error (G_ "")) + (display (apply string-append %strings)) + (throw 'bad! var)))) + +;; if strings is (list "auth" "for any" "from local") +;; Then this will return "Try \"auth\", \"for any\", or \"from local\". +(define (try-string strings) + (string-append "Try " + (let loop ((strings strings)) + (cond ((= 1 (length strings)) + (string-append + "or \"" (car strings) "\".\n")) + (else + (string-append + "\"" (car strings) "\", " + (loop (cdr strings)))))))) + +;; suppose string is "for anys" +;; and strings is (list "for any" "for local" "for domain") +;; then hint-string will return "Did you mean "for any"?" +(define* (hint-string string strings + #:key (fieldname #f)) + (define str (string-closest string strings)) + (if (not str) + (try-string strings) + (if fieldname + (string-append "Did you mean (" fieldname " \"" + str "\") ?\n") + (string-append "Did you mean \"" str "\" ?\n")))) + +;; this is used for sanitizing fieldname 'options' +(define (contains-duplicate? list) + (if (null? list) + #f + (or + ;; check if (car list) is in (cdr list) + (any (lambda (var) + (equal? var (car list))) + (cdr list)) + ;; check if (cdr list) contains duplicate + (contains-duplicate? (cdr list))))) + +(define* (variable->string var #:key (append "") (postpend " ")) + (let ((var (if (number? var) + (number->string var) + var))) + (if var + (string-append append var postpend) + ""))) + +;;; Various functions to check that lists are of the appropriate type. + +;; given a list and procedure, this tests that each element of list is of type +;; ie: (list-of-type? list string?) tests each list is of type string. +(define (list-of-type? list proc?) + (if (and (list? list) + (not (null? list))) + (let loop ((list list)) + (if (null? list) + #t + (if (proc? (car list)) + (loop (cdr list)) + #f))) + #f)) + +(define (list-of-strings? list) + (list-of-type? list string?)) + +(define (list-of-interface? list) + (list-of-type? list opensmtpd-interface?)) + +(define (list-of-opensmtpd-option? list) + (list-of-type? + list opensmtpd-option?)) + +(define (list-of-opensmtpd-ca? list) + (list-of-type? list opensmtpd-ca?)) + +(define (list-of-opensmtpd-pki? list) + (list-of-type? list opensmtpd-pki?)) + +(define (list-of-opensmtpd-match? list) + (list-of-type? list opensmtpd-match?)) + +(define* (list-of-strings->string list + #:key + (string-delimiter ", ") + (postpend "") + (append "") + (drop-right-number 2)) + (string-drop-right + (string-append (let loop ((list list)) + (if (null? list) + "" + (string-append append (car list) postpend + string-delimiter + (loop (cdr list))))) + append) + drop-right-number)) + +;; TODO I should probably change this to alist, because that's what this is. +(define (assoc-list? assoc-list) + (list-of-type? assoc-list + (lambda (pair) + (and (pair? pair) + (string? (car pair)) + (string? (cdr pair)) + (<= 1 (string-length (car pair))) + (<= 1 (string-length (cdr pair))))))) + +(define (nested-list? list) + (every (lambda (element) + (and + (list-of-strings? element) + (< 1 (length element)))) + list)) + +;; this procedure takes in one argument. +;; if that argument is an whose fieldname 'values' is +;; an assoc-list, then it returns #t, #f if otherwise. +(define (tables-data-are-assoc-list? table) + (if (not (opensmtpd-table? table)) + #f + (assoc-list? (opensmtpd-table-data table)))) + +;; this procedure takes in one argument +;; if that argument is an whose fieldname 'values' is a +;; list of strings, then it returns #t, #f if otherwise. +(define (tables-data-are-a-list-of-strings? table) + (if (not (opensmtpd-table? table)) + #f + (and (list-of-strings? (opensmtpd-table-data table))))) + +;; This procedures takes in an +;; if that table a list of lists of strings eg: +;; (list (list "cat") (list "dog")) +;; then this returns #t, otherwise false. +(define (tables-data-are-a-nested-list-of-strings? table) + (cond ((false? (opensmtpd-table-data table)) + #f) + ((not (list? (opensmtpd-table-data table))) + #f) + (else + (nested-list? (opensmtpd-table-data table))))) + +;;; The following functions convert various records into strings. + +;; these next few functions help me to turn
s +;; into strings suitable to fit into "opensmtpd.conf". +(define (assoc-list->string assoc-list) + (string-drop-right + (let loop ((assoc-list assoc-list)) + (if (null? assoc-list) + "" + ;; pair is (cons "hello" "world") -> ("hello" . "world") + (let ((pair (car assoc-list))) + (string-append + "\"" (car pair) "\"" + " = " + "\"" (cdr pair) "\"" + ", " + (loop (cdr assoc-list)))))) + 2)) + +;; can be of type: (quote list-of-strings) or (quote assoc-list) +;; this will output a string that looks like: +;; table <"mytable"> { "ludo"="ludo@gnu.org" } +(define (opensmtpd-table->string table) + (string-append "table \"" (opensmtpd-table-name table) "\" " + (cond ((tables-data-are-a-list-of-strings? table) + (string-append "{ " + (list-of-strings->string + (opensmtpd-table-data table) + #:append "\"" + #:drop-right-number 3 + #:postpend "\"") " }")) + ((tables-data-are-assoc-list? table) + (string-append "{ " + (assoc-list->string + (opensmtpd-table-data table)) " }")) + (else (throw 'youMessedUp table))) + " \n")) + +;; will output something like: +;; <"mytable"> +(define (opensmtpd-table-name->string table) + (string-append "<\"" (opensmtpd-table-name table) "\">")) + +(define (opensmtpd-interface->string record) + (string-append + "listen on " + (opensmtpd-interface-interface record) " " + (let* ((hostname (opensmtpd-interface-hostname record)) + (hostnames (if (opensmtpd-interface-hostnames record) + (opensmtpd-table-name + (opensmtpd-interface-hostnames record)) + #f)) + (filters (opensmtpd-interface-filters record)) + (filter-name (if filters + (if (< 1 (length filters)) + (generate-filter-chain-name filters) + (if (opensmtpd-filter? (car filters)) + (opensmtpd-filter-name (car filters)) + (opensmtpd-filter-phase-name + (car filters)))) + #f)) + (mask-src (opensmtpd-interface-mask-src record)) + (tag (opensmtpd-interface-tag record)) + (senders (opensmtpd-interface-senders record)) + (masquerade (opensmtpd-interface-masquerade record)) + (secure-connection (opensmtpd-interface-secure-connection record)) + (port (opensmtpd-interface-port record)) + (pki (opensmtpd-interface-pki record)) + (auth (opensmtpd-interface-auth record)) + (auth-optional (opensmtpd-interface-auth-optional record))) + (string-append + (if mask-src + (string-append "mask-src ") + "") + (variable->string hostname #:append "hostname ") + (variable->string hostnames #:append "hostnames <" #:postpend "> ") + (variable->string filter-name #:append "filter \"" #:postpend "\" ") + (variable->string tag #:append "tag \"" #:postpend "\" ") + (if secure-connection + (cond ((string=? "smtps" secure-connection) + "smtps ") + ((string=? "tls" secure-connection) + "tls ") + ((string=? "tls-require" secure-connection) + "tls-require ") + ((string=? "tls-require-verify" secure-connection) + "tls-require verify ")) + "") + (if senders + (string-append "senders <\"" (opensmtpd-table-name senders) "\"> " + (if masquerade + "masquerade " + "")) + "") + (variable->string port #:append "port " #:postpend " ") + (if pki + (variable->string (opensmtpd-pki-domain pki) #:append "pki ") + "") + (if auth + (string-append "auth " + (if (opensmtpd-table? auth) + (string-append + (opensmtpd-table-name->string auth)) + "")) + "") + (if auth-optional + (string-append "auth-optional " + (if (opensmtpd-table? auth-optional) + (string-append + "<\"" + (opensmtpd-table-name->string auth-optional) + "\">") + "")) + "") + "\n")))) + +(define (opensmtpd-socket->string record) + (string-append + "listen on socket " + (let* ((filters (opensmtpd-socket-filters record)) + (filter-name (if filters + (if (< 1 (length filters)) + (generate-filter-chain-name filters) + (if (opensmtpd-filter? (car filters)) + (opensmtpd-filter-name (car filters)) + (opensmtpd-filter-phase-name + (car filters)))) + #f)) + (mask-src (opensmtpd-socket-mask-src record)) + (tag (opensmtpd-socket-tag record))) + (string-append + (if mask-src + (string-append "mask-src ") + "") + (variable->string filter-name #:append "filter \"" #:postpend "\" ") + (variable->string tag #:append "tag \"" #:postpend "\" ") + "\n")))) + +(define (opensmtpd-relay->string record) + (let ((backup (opensmtpd-relay-backup record)) + (backup-mx (opensmtpd-relay-backup-mx record)) + (helo (opensmtpd-relay-helo record)) + ;; helo-src can either be a string IP address or an + (helo-src (if (opensmtpd-relay-helo-src record) + (if (string? (opensmtpd-relay-helo-src record)) + (opensmtpd-relay-helo-src record) + (string-append "<\"" + (opensmtpd-table-name + (opensmtpd-relay-src record)) + "\">")) + #f)) + (domain (if (opensmtpd-relay-domain record) + (opensmtpd-table-name + (opensmtpd-relay-domain record)) + #f)) + (host (opensmtpd-relay-host record)) + (name (opensmtpd-relay-name record)) + (pki (if (opensmtpd-relay-pki record) + (opensmtpd-pki-domain (opensmtpd-relay-pki record)) + #f)) + (srs (opensmtpd-relay-srs record)) + (tls (opensmtpd-relay-tls record)) + (auth (if (opensmtpd-relay-auth record) + (opensmtpd-table-name + (opensmtpd-relay-auth record)) + #f)) + (mail-from (opensmtpd-relay-mail-from record)) + ;; src can either be a string IP address or an + (src (if (opensmtpd-relay-src record) + (if (string? (opensmtpd-relay-src record)) + (opensmtpd-relay-src record) + (string-append "<\"" + (opensmtpd-table-name + (opensmtpd-relay-src record)) + "\">")) + #f))) + + (string-append + "\"" + name + "\" " "relay " + ;;FIXME should I always quote the host fieldname? + ;; do I need to quote localhost via "localhost" ? + (variable->string host #:append "host \"" #:postpend "\" ") + (variable->string backup) + (variable->string backup-mx #:append "backup mx ") + (variable->string helo #:append "helo ") + (variable->string helo-src #:append "helo-src ") + (variable->string domain #:append "domain <\"" #:postpend "\"> ") + (variable->string host #:append "host ") + (variable->string pki #:append "pki ") + (variable->string srs) + (variable->string tls #:append "tls ") + (variable->string auth #:append "auth <\"" #:postpend "\"> ") + (variable->string mail-from #:append "mail-from ") + (variable->string src #:append "src ") + "\n"))) + +(define (opensmtpd-lmtp->string record) + (string-append "lmtp " + (opensmtpd-lmtp-destination record) + (if (opensmtpd-lmtp-rcpt-to record) + (begin + " " (opensmtpd-lmtp-rcpt-to record)) + ""))) + +(define (opensmtpd-mda->string record) + (string-append "mda " + (opensmtpd-mda-command record) " ")) + +(define (opensmtpd-maildir->string record) + (string-append "maildir " + "\"" + (if (opensmtpd-maildir-pathname record) + (opensmtpd-maildir-pathname record) + "~/Maildir") + "\"" + (if (opensmtpd-maildir-junk record) + " junk " + " "))) + +(define (opensmtpd-local-delivery->string record) + (let ((name (opensmtpd-local-delivery-name record)) + (method (opensmtpd-local-delivery-method record)) + (alias (if (opensmtpd-local-delivery-alias record) + (opensmtpd-table-name + (opensmtpd-local-delivery-alias record)) + #f)) + (ttl (opensmtpd-local-delivery-ttl record)) + (user (opensmtpd-local-delivery-user record)) + (userbase (if (opensmtpd-local-delivery-userbase record) + (opensmtpd-table-name + (opensmtpd-local-delivery-userbase record)) + #f)) + (virtual (if (opensmtpd-local-delivery-virtual record) + (opensmtpd-table-name + (opensmtpd-local-delivery-virtual record)) + #f)) + (wrapper (opensmtpd-local-delivery-wrapper record))) + (string-append + "\"" name "\" " + (cond ((string? method) + (string-append method " ")) + ((opensmtpd-mda? method) + (opensmtpd-mda->string method)) + ((opensmtpd-lmtp? method) + (opensmtpd-lmtp->string method)) + ((opensmtpd-maildir? method) + (opensmtpd-maildir->string method))) + ;; FIXME/TODO support specifying alias file:/path/to/alias-file ? + ;; I do not think that is something that I can do... + (variable->string alias #:append "alias <\"" #:postpend "\"> ") + (variable->string ttl #:append "ttl ") + (variable->string user #:append "user ") + (variable->string userbase #:append "userbase <\"" #:postpend "\"> ") + (variable->string virtual #:append "virtual <\"" #:postpend "\"> ") + (variable->string wrapper #:append "wrapper ")))) + +;; this function turns both opensmtpd-local-delivery and +;; opensmtpd-relay into strings. +(define (opensmtpd-action->string record) + (string-append "action " + (cond ((opensmtpd-local-delivery? record) + (opensmtpd-local-delivery->string record)) + ((opensmtpd-relay? record) + (opensmtpd-relay->string record))) + " \n")) + +;; this turns option records found in into strings. +(define* (opensmtpd-option->string record + #:key + (space-after-! #f)) + (let ((bool (opensmtpd-option-bool record)) + (option (opensmtpd-option-option record)) + (regex (opensmtpd-option-regex record)) + (data (opensmtpd-option-data record))) + (string-append + (if (false? bool) + (if space-after-! + "! " + "!") + "") + option " " + (if regex + "regex " + "") + (if data + (if (opensmtpd-table? data) + (string-append + (opensmtpd-table-name->string data) " ") + (string-append data " ")) + "")))) + +(define (opensmtpd-match->string record) + (string-append "match " + (let* ((action (opensmtpd-match-action record)) + (name (cond ((opensmtpd-relay? action) + (opensmtpd-relay-name action)) + ((opensmtpd-local-delivery? action) + (opensmtpd-local-delivery-name action)) + (else 'reject))) + (options (opensmtpd-match-options record))) + (string-append + (if options + (apply string-append + (map opensmtpd-option->string options)) + "") + (if (string? name) + (string-append "action " "\"" name "\" ") + "reject ") + "\n")))) + +(define (opensmtpd-ca->string record) + (string-append "ca " (opensmtpd-ca-name record) " " + "cert \"" (opensmtpd-ca-file record) "\"\n")) + +(define (opensmtpd-pki->string record) + (let ((domain (opensmtpd-pki-domain record)) + (cert (opensmtpd-pki-cert record)) + (key (opensmtpd-pki-key record)) + (dhe (opensmtpd-pki-dhe record))) + (string-append "pki " domain " " "cert \"" cert "\" \n" + "pki " domain " " "key \"" key "\" \n" + (if dhe + (string-append + "pki " domain " " "dhe " dhe "\n") + "")))) + +(define (generate-filter-chain-name list-of-filters) + (string-drop-right (apply string-append + (flatten + (map (lambda (filter) + (list + (if (opensmtpd-filter? filter) + (opensmtpd-filter-name filter) + (opensmtpd-filter-phase-name filter)) + "-")) + list-of-filters))) + 1)) + +(define (opensmtpd-filter->list-of-strings-and-gexps record) + (list "filter " + "\"" (opensmtpd-filter-name record) "\" " + (if (opensmtpd-filter-exec record) + "proc-exec " + "proc ") + "\"" (opensmtpd-filter-proc record) "\"" + "\n\n")) + +;; this procedure takes in a list of and +;; . It returns a string of the form: +;; filter "uniqueName" chain chain { "filter-name", "filter-name2" [, ...]} +(define (opensmtpd-filter-chain->string list-of-filters) + (string-append "filter \"" + (generate-filter-chain-name list-of-filters) + "\" " + "chain {" + (string-drop-right + (apply string-append + (flatten + (map (lambda (filter) + (list + "\"" + (if (opensmtpd-filter? filter) + (opensmtpd-filter-name filter) + (opensmtpd-filter-phase-name filter)) + "\", ")) + list-of-filters))) + 2) + "}\n\n")) + +(define (opensmtpd-filter-phase->string record) + (let ((name (opensmtpd-filter-phase-name record)) + (phase (opensmtpd-filter-phase-phase record)) + (decision (opensmtpd-filter-phase-decision record)) + (options (opensmtpd-filter-phase-options record)) + (message (opensmtpd-filter-phase-message record)) + (value (opensmtpd-filter-phase-value record))) + (string-append "filter " + "\"" name "\" " + "phase " phase " " + "match " + (apply string-append ; turn the options into a string + (flatten + (map (lambda (option) + (opensmtpd-option->string + option #:space-after-! #f)) + options))) + " " + decision " " + (if (member decision (list "reject" "disconnect")) + (string-append "\"" message "\"") + "") + (if (string=? "rewrite" decision) + (string-append "rewrite " (number->string value)) + "") + "\n\n"))) + +;; in the next procedure, the variable 'filters' is a list of +;; , , and filter chains, which are +;; lists that look like: +;; (list (opensmtpd-filter ...) (opensmtpd-filter-phase ...) +;; (opensmtpd-filter-phase ...) (opensmtpd-filter ...)) +;; This function converts (get-opensmtpd-filters ) +;; to a string. +;; Consider if a user passed in a valid , +;; so that (get-opensmtpd-filters (opensmtpd-configuration)) returns +;; (list (opensmtpd-filter +;; (name "rspamd") +;; (proc "rspamd")) +;; ;; this is a listen-on, with a filter-chain. +;; (list (opensmtpd-filter-phase +;; (name "dkimsign") +;; ...) +;; (opensmtpd-filter +;; (name "rspamd") +;; (proc "rspamd")))) +;; +;; (we will call the above list "total filters"): +;; did you notice that filter "rspamd" is listed twice? Once by itself, and +;; once again in a filter chain. How do you make sure that it is NOT printed +;; twice in smtpd.conf? +;; 1st flatten "total filters", then remove its duplicates so that we +;; may print the s and s. +;; 2nd now we go through "total filters", and we only print the filter-chains. +(define (opensmtpd-filters->list-of-strings-and-gexps filters) + ;; first print the unique s and s. + ;; then print the filter-chains. + ;; to do this: flatten filters, then remove duplicates. + (flatten + (list + ;; TODO for funsies, try to figure out how to list the filter-phases and + ;; filters in one go. I tried it earlier, and it broke the service. + ;; Why? + ;; + + ;; print the filter-phases + (apply string-append + (map (lambda (filter) + (cond ((opensmtpd-filter-phase? filter) + (opensmtpd-filter-phase->string filter)) + (else ""))) + (delete-duplicates (flatten filters)))) + + ;; list the filters that may be gexps + (map (lambda (filter) + (cond ((opensmtpd-filter? filter) + (opensmtpd-filter->list-of-strings-and-gexps filter)) + (else ""))) + (delete-duplicates (flatten filters))) + + ;; now we have to print the filter chains. + (apply string-append + (map (lambda (filter) + (cond ((list? filter) + (opensmtpd-filter-chain->string filter)) + (else ; you are a + ""))) + filters))))) + +(define (opensmtpd-configuration-includes->string string) + (string-append + "include \"" string "\"\n")) + +(define (opensmtpd-configuration-srs->string record) + (let ((key (opensmtpd-srs-key record)) + (backup-key (opensmtpd-srs-backup-key record)) + (ttl-delay (opensmtpd-srs-ttl-delay record))) + (string-append + (variable->string key #:append "srs key " #:postpend "\n") + (variable->string backup-key #:append "srs key backup " #:postpend "\n") + (variable->string ttl-delay #:append "srs ttl " #:postpend "\n") + "\n"))) + +;; TODO make sure all options here work! I just fixed limit-max-rcpt! +(define (opensmtpd-smtp->string record) + (let ((ciphers (opensmtpd-smtp-ciphers record)) + (limit-max-mails (opensmtpd-smtp-limit-max-mails record)) + (limit-max-rcpt (opensmtpd-smtp-limit-max-rcpt record)) + (max-message-size (opensmtpd-smtp-max-message-size record)) + (sub-addr-delim (opensmtpd-smtp-sub-addr-delim record))) + (string-append + (variable->string ciphers #:append "smtp ciphers " #:postpend "\n") + (variable->string limit-max-mails + #:append "smtp limit max-mails " #:postpend "\n") + (variable->string limit-max-rcpt + #:append "smtp limit max-rcpt " #:postpend "\n") + (variable->string max-message-size + #:append "smtp max-message-size " #:postpend "\n") + (variable->string sub-addr-delim + #:append "smtp sub-addr-delim " #:postpend "\n") + "\n"))) + +(define (opensmtpd-configuration-queue->string record) + (let ((compression (opensmtpd-queue-compression record)) + (encryption (opensmtpd-queue-encryption record)) + (ttl-delay (opensmtpd-queue-ttl-delay record))) + (string-append + (if compression + "queue compression\n" + "") + (if encryption + (string-append + "queue encryption " + (if (not (boolean? encryption)) + encryption + "") + "\n") + "") + (if ttl-delay + (string-append "queue ttl" ttl-delay "\n") + "")))) + +;; build a list of from +;; opensmtpd-configuration-matches, which is a list of . +;; Each has a fieldname 'action', which accepts +;; an . +(define (get-opensmtpd-actions record) + (define opensmtpd-actions + (let loop ((list (opensmtpd-configuration-matches record))) + (if (null? list) + '() + (cons (opensmtpd-match-action (car list)) + (loop (cdr list)))))) + (delete-duplicates (append opensmtpd-actions))) + +;; build a list of opensmtpd-pkis from +;; opensmtpd-configuration-interfaces and +;; get-opensmtpd-actions +(define (get-opensmtpd-pkis record) + ;; TODO/FIXME/maybe/wishlist could get-opensmtpd-actions -> NOT have an + ;; opensmtpd-relay? + ;; I think so. And if it did NOT have a relay configuration, then + ;; action-pkis would be '() when it needs to be #f. because if the + ;; opensmtpd-configuration has NO pkis, then this function will + ;; return '(), when it should return #f. If it returns '(), then + ;; opensmtpd-configuration-fieldname->string will + ;; print the string "\n" instead of "" + (define action-pkis + (let loop1 ((list (get-opensmtpd-actions record))) + (if (null? list) + '() + (if (and (opensmtpd-relay? (car list)) + (opensmtpd-relay-pki (car list))) + (cons (opensmtpd-relay-pki (car list)) + (loop1 (cdr list))) + (loop1 (cdr list)))))) + ;; FIXME/TODO/maybe/wishlist + ;; this could be #f aka left blank. aka there are no interface records + ;; with pkis. aka there are no lines in the configuration like: + ;; listen on eth0 tls pki smtp.gnucode.me + ;; in that case the smtpd.conf will have an extra "\n" + (define listen-on-pkis + (let loop2 ((list (opensmtpd-configuration-interfaces record))) + (if (null? list) + '() + (if (opensmtpd-interface-pki (car list)) + (cons (opensmtpd-interface-pki (car list)) + (loop2 (cdr list))) + (loop2 (cdr list)))))) + (delete-duplicates (append action-pkis listen-on-pkis))) + +;; takes in a and returns a list whose +;; elements are , , +;; and a filter-chain. +;; It returns a list of and/or +;; here's an example of what this procedure might return: +;; (list (opensmtpd-filter...) (opensmtpd-filter-phase ...) +;; (openmstpd-filter ...) (opensmtpd-filter-phase ...) +;; ;; this next list is a filter-chain. +;; (list (opensmtpd-filter-phase ...) (opensmtpd-filter...))) +;; +;; This procedure handles filter chains a little odd. +(define (get-opensmtpd-filters record) + (define socket-filters + (if (and (opensmtpd-configuration-socket record) + (opensmtpd-socket-filters + (opensmtpd-configuration-socket record))) + (opensmtpd-socket-filters (opensmtpd-configuration-socket record)) + '())) + (define list-of-interfaces + (if (opensmtpd-configuration-interfaces record) + (opensmtpd-configuration-interfaces record) + '())) + + (delete-duplicates + (append + (remove boolean? + (map-in-order + ;; get the filters found in the s + (lambda (interface-or-socket-record) + (if (and + (opensmtpd-interface-filters interface-or-socket-record) + (= 1 (length (opensmtpd-interface-filters + interface-or-socket-record)))) + ;; this next line returns an + (car (opensmtpd-interface-filters + interface-or-socket-record)) + ;; this next line returns a filter chain. + (opensmtpd-interface-filters interface-or-socket-record))) + list-of-interfaces)) + socket-filters))) + +(define (flatten . lst) + "Return a list that recursively concatenates all sub-lists of LST." + (define (flatten1 head out) + (if (list? head) + (fold-right flatten1 out head) + (cons head out))) + (fold-right flatten1 '() lst)) + +;; This function takes in a record, or list, or anything, and returns +;; a list of s assuming the thing you passed into it had +;; any s. +;; +;; is object record? call func on it's fieldnames +;; is object list? loop through it's fieldnames calling func on it's records +;; is object #f or string? or '()? -> #f +(define (get-opensmtpd-tables value) + (delete-duplicates + (remove boolean? + (flatten ;; turn (list '(1) '(2 '(3))) -> '(1 2 3) + (cond ((opensmtpd-table? value) + value) + ((record? value) + (let* ((record-type (record-type-descriptor value)) + (list-of-record-fieldnames + (record-type-fields record-type))) + (map (lambda (fieldname) + (get-opensmtpd-tables + ((record-accessor record-type fieldname) + value))) + list-of-record-fieldnames))) + ((and (list? value) (not (null? value))) + (map get-opensmtpd-tables value)) + (else #f)))))) + +(define (opensmtpd-configuration-fieldname->string + record fieldname-accessor record->string) + (if (fieldname-accessor record) + (begin + (string-append + (list-of-records->string (fieldname-accessor record) + record->string) "\n")) + "")) + +(define (list-of-records->string list-of-records record->string) + (string-append + (cond ((not (list? list-of-records)) + (record->string list-of-records)) + (else + (let loop ((list list-of-records)) + (if (null? list) + "" + (string-append + (record->string (car list)) + (loop (cdr list))))))))) + +(define (opensmtpd-configuration->string record) + ;; should I use this named let, or should I give this a name, or + ;; not use it at all... + ;; eg: + ;; (write-all-fieldnames + ;; (list (cons fieldname fieldname->string) + ;; (cons fieldname2 fieldname->string))) + ;; (let loop ([list + ;; (list + ;; (cons opensmtpd-configuration-includes + ;; (lambda (string) + ;; (string-append + ;; "include \"" string "\"\n"))) + ;; (cons opensmtpd-configuration-smtp opensmtpd-smtp->string) + ;; (cons opensmtpd-configuration-srs opensmtpd-srs->string))]) + ;; (if (null? list) + ;; "" + ;; (string-append + ;; (opensmtpd-configuration-fieldname->string record + ;; (caar list) + ;; (cdar list)) + ;; (loop (cdr list))))) + (string-append + (opensmtpd-configuration-fieldname->string + record opensmtpd-configuration-bounce + (lambda (%bounce) + (if %bounce + (list-of-strings->string %bounce) + ""))) + (opensmtpd-configuration-fieldname->string record + opensmtpd-configuration-smtp + opensmtpd-smtp->string) + (opensmtpd-configuration-fieldname->string + record + opensmtpd-configuration-srs + opensmtpd-configuration-srs->string) + (opensmtpd-configuration-fieldname->string + record + opensmtpd-configuration-queue + opensmtpd-configuration-queue->string) + ;; write out the mta-max-deferred + (opensmtpd-configuration-fieldname->string + record opensmtpd-configuration-mta-max-deferred + (lambda (var) + (string-append "mta max-deferred " + (number->string + (opensmtpd-configuration-mta-max-deferred record)) + "\n"))) + ;;write out all the tables + (opensmtpd-configuration-fieldname->string record get-opensmtpd-tables + opensmtpd-table->string) + ;; write out all the cas + (opensmtpd-configuration-fieldname->string record + opensmtpd-configuration-cas + opensmtpd-ca->string) + ;; write out all the pkis + (opensmtpd-configuration-fieldname->string record get-opensmtpd-pkis + opensmtpd-pki->string) + ;; write all of the interface and socket records + (opensmtpd-configuration-fieldname->string + record + opensmtpd-configuration-interfaces + opensmtpd-interface->string) + (opensmtpd-configuration-fieldname->string record + opensmtpd-configuration-socket + opensmtpd-socket->string) + ;; write all the actions + (opensmtpd-configuration-fieldname->string record get-opensmtpd-actions + opensmtpd-action->string) + ;; write all of the matches + (opensmtpd-configuration-fieldname->string record + opensmtpd-configuration-matches + opensmtpd-match->string))) + +;; FIXME/TODO should I use format here srfi-28 ? +;; web.scm nginx does a (format #f "string" "another string") +;; this could be a list like +;; (list +;; (file-append opensmtpd-dkimsign "/libexec/filter") +;; "-d gnucode.me -s /path/to/selector.cert") +;; Then opensmtpd-configuration->mixed-text-file could be rewritten to be +;; something like +;; (mixed-text-file +;; (eval `(string-append (opensmtpd-configuration-fieldname->string ...)) +;; (gnu services mail))) +(define (opensmtpd-configuration->mixed-text-file record) + (apply mixed-text-file "smtpd.conf" + (flatten (list + ;; write out the includes + (opensmtpd-configuration-fieldname->string + record + opensmtpd-configuration-includes + opensmtpd-configuration-includes->string) + ;; TODO should I change the below line of code into these + ;; two lines of code? + ;;(opensmtpd-configuration-fieldname->string + ;; record get-opensmtpd-filters-and-filter-phases + ;; opensmtpd-filter-and-filter-phase->string) + ;;(opensmtpd-configuration-fieldname->string + ;; record get-opensmtpd-filter-chains + ;; opensmtpd-filter-chain->string) + ;; write out all the filters + (opensmtpd-filters->list-of-strings-and-gexps + (get-opensmtpd-filters record)) + (opensmtpd-configuration->string record))))) + (define %default-opensmtpd-config-file (plain-file "smtpd.conf" " listen on lo @@ -1668,18 +4140,37 @@ (define %default-opensmtpd-config-file match from local for any action outbound ")) -(define opensmtpd-shepherd-service - (match-lambda - (($ package config-file) - (list (shepherd-service +(define (opensmtpd-shepherd-service config) + (list (shepherd-service (provision '(smtpd)) (requirement '(loopback)) (documentation "Run the OpenSMTPD daemon.") - (start (let ((smtpd (file-append package "/sbin/smtpd"))) - #~(make-forkexec-constructor - (list #$smtpd "-f" #$config-file) - #:pid-file "/var/run/smtpd.pid"))) - (stop #~(make-kill-destructor))))))) + (start + (let ((smtpd (file-append + (opensmtpd-configuration-package config) + "/sbin/smtpd"))) + #~(make-forkexec-constructor + (list #$smtpd "-f" + (or + #$(opensmtpd-configuration-config-file config) + #$(opensmtpd-configuration->mixed-text-file config))) + #:pid-file "/var/run/smtpd.pid"))) + (stop #~(make-kill-destructor))))) + +;; TODO why does the below NOT work? +;(define (opensmtpd-shepherd-service config) +; (match-lambda +; (($ package config-file) +; (list (shepherd-service +; (provision '(smtpd)) +; (requirement '(loopback)) +; (documentation "Run the OpenSMTPD daemon.") +; (start (let ((smtpd (file-append package "/sbin/smtpd"))) +; #~(make-forkexec-constructor +; (list #$smtpd "-f" (or #$config-file +; #$(opensmtpd-configuration->mixed-text-file config))) +; #:pid-file "/var/run/smtpd.pid"))) +; (stop #~(make-kill-destructor))))))) (define %opensmtpd-accounts (list (user-group @@ -1700,10 +4191,10 @@ (define %opensmtpd-accounts (home-directory "/var/empty") (shell (file-append shadow "/sbin/nologin"))))) -(define opensmtpd-activation - (match-lambda - (($ package config-file) - (let ((smtpd (file-append package "/sbin/smtpd"))) +(define (opensmtpd-activation config) + (let ((smtpd (file-append (opensmtpd-configuration-package config) "/sbin/smtpd")) + (config-file (opensmtpd-configuration-config-file config)) + (configuration (opensmtpd-configuration->mixed-text-file config))) #~(begin (use-modules (guix build utils)) ;; Create mbox and spool directories. @@ -1711,7 +4202,36 @@ (define opensmtpd-activation (mkdir-p "/var/spool/smtpd") (chmod "/var/spool/smtpd" #o711) (mkdir-p "/var/spool/mail") - (chmod "/var/spool/mail" #o711)))))) + (chmod "/var/spool/mail" #o711) + (display (string-append "checking syntax of " + (or + #$config-file + #$configuration) + "\n")) + (system* #$smtpd "-nf" + (or + #$config-file + #$configuration))))) + +;; TODO why does this not work? +;(define (opensmtpd-activation config) +; (match-lambda +; (($ package config-file) +; (let ((smtpd (file-append package "/sbin/smtpd")) +; (configuration (opensmtpd-configuration->mixed-text-file config))) +; #~(begin +; (use-modules (guix build utils)) + ;; Create mbox and spool directories. +; (mkdir-p "/var/mail") +; (mkdir-p "/var/spool/smtpd") +; (chmod "/var/spool/smtpd" #o711) +; (mkdir-p "/var/spool/mail") +; (chmod "/var/spool/mail" #o711) +; (display (string-append "checking syntax of " +; (or +; #$config-file +; #$configuration) +; "\n"))))))) (define %opensmtpd-pam-services (list (unix-pam-service "smtpd"))) diff --git a/gnu/tests/mail.scm b/gnu/tests/mail.scm index f13751b72f..f9bd96402b 100644 --- a/gnu/tests/mail.scm +++ b/gnu/tests/mail.scm @@ -37,6 +37,7 @@ (define-module (gnu tests mail) #:use-module (guix gexp) #:use-module (guix store) #:use-module (ice-9 ftw) + #:use-module (srfi srfi-64) #:export (%test-opensmtpd %test-exim %test-dovecot @@ -165,6 +166,718 @@ (define %test-opensmtpd (description "Send an email to a running OpenSMTPD server.") (value (run-opensmtpd-test)))) +;; trying to create a bad record, should result in an error. +;; this function should not be able return, instead it should throw an error +;(define (create-bad-record record) + ;; TODO why is this not working +; (with-output-to-port (%make-void-port "w") +; (lambda () (when record #f)))) + +;; if this caller function is reached, then trying to create the bad record +;; resulted in an error. So return true. +(define (return-true error arg) + #t) + +(define (return-false error arg) + #f) + +(define (test-good-record func) + (catch #t func return-false)) + +(define (run-opensmtpd-record-sanitation-test) + ;;(with-output-to-port (%make-void-port "w") + ;; (lambda () + (test-begin "run-opensmtpd-record-sanitation-test") + + (test-error "(interface (filters ...)) has two filters with the same name." + #t + (let ((dkimsign + (opensmtpd-filter + (name "dkimsign") + (exec #t) + (proc + (list + (file-append + opensmtpd-filter-dkimsign + "/libexec/opensmtpd/filter-dkimsign") + " -d gnucode.me -s 2021-09-22 -c " + "relaxed/relaxed -k " + "rando string" + "/etc/dkim/private.key " + "user nobody group nogroup"))))) + (opensmtpd-interface + (interface "lo") + (filters (list + dkimsign + dkimsign))))) + + ;; duplicate filter names. The filters are different, the fix + ;; is to change one filter's 'name' field. + (test-error "Test cannot have two filters with the same name." + #t + (opensmtpd-interface + (filters (list + (opensmtpd-filter-phase + (name "src") + (phase "connect") + (options + (list + (opensmtpd-option + (option "fcrdns") + (bool #f)))) + (decision "junk")) + (opensmtpd-filter-phase + (name "src") + (phase "helo") + (options + (list + (opensmtpd-option + (option "rdns") + (bool #f)))) + (decision "reject") + (message "433 no rdns.")))))) + + (test-error "(filter (proc ...)) is a list of strings or gexps, NOT numbers." + #t + (opensmtpd-filter + (name "dkimsign") + (proc + (list + (file-append opensmtpd-filter-dkimsign + "/libexec/opensmtpd/filter-dkimsign") + " -d gnucode.me -s 2021-09-22 -c relaxed/relaxed -k " + 5 + "/etc/dkim/private.key " + "user nobody group nogroup")) + (exec #t))) + + (test-error "Test fieldname 'phase' has an wrong phase name." + #t + (opensmtpd-filter-phase + (name "filter") + (phase "wrongString") + (decision "bypass") + (options + (list + (opensmtpd-option + (option "auth")))))) + + (test-error + "(filter-phase (decision \"reject\")) requires a 'message'." + #t + (opensmtpd-interface + (filters (list + (opensmtpd-filter-phase + (name "src") + (phase "connect") + (options + (list + (opensmtpd-option + (option "src") + (data (opensmtpd-table + (name "src-table") + (data (list "cat" "hat"))))))) + (decision "reject")))))) + + (test-error (string-append + "Test fieldname 'decision' " + "w/ value \"reject\" and \"disconnect\" requires a 'message'." + " The message must begin with 4xx or 5xx.") + #t + (opensmtpd-interface + (filters (list + (opensmtpd-filter-phase + (name "src") + (phase "connect") + (options + (list + (opensmtpd-option + (option "src") + (data (opensmtpd-table + (name "src-table") + (data (list "cat" "hat"))))))) + (decision "reject") + (message "322 Bad data!")))))) + + (test-error + "(filter-phase \"rewrite\") requires fieldname 'value' to have a number." + #t + (opensmtpd-interface + (filters + (list + (opensmtpd-filter-phase + (name "noFRDNS") + (phase "commit") + (options (list (opensmtpd-option + (option "fcrdns") + (bool #f)))) + (decision "rewrite")))))) + + (define option-string1/2 (list-ref (list "junk" "bypass") + (random 2))) + (test-error + (string-append "(filter-phase \"decision\" with values 'junk' or 'bypass'," + " then fieldname 'message' and 'value' must be blank.") + #t + (opensmtpd-interface + (filters + (list + (opensmtpd-filter-phase + (name "noFRDNS") + (phase "commit") + (options (list (opensmtpd-option + (option "fcrdns") + ))) + (decision option-string1/2) + (message "This is not a good email.")))))) + + (test-error "You cannot junk an email on phase commit." + #t + (opensmtpd-interface + (filters + (list + (opensmtpd-filter-phase + (name "junk-after-commit") + (options (list (opensmtpd-option + (option "fcrdns")))) + (phase "commit") + (decision "junk")))))) + + (define (good-interface1) + (opensmtpd-interface + (senders + (opensmtpd-table + (name "senders") + (data '(("joshua" . "joshua@dismail.de"))))) + (masquerade #t))) + + (test-assert "good interface" (test-good-record good-interface1)) + + (test-error "Test has 2 duplicate options." + #t + (opensmtpd-filter-phase + (name "invalid-fcrdns") + (phase "connect") + (options + (list (opensmtpd-option + (option "fcrdns")) + (opensmtpd-option + (option "fcrdns")))) + (decision "reject") + (message "422 No valid fcrdns."))) + + (test-error "Test option 'src' requires a table." + #t + (opensmtpd-filter-phase + (name "filter") + (phase "helo") + (decision "bypass") + (options + (list + (opensmtpd-option + (option "src")))))) + + (test-error "Test option 'fcrdns' cannot have a table." + #t + (opensmtpd-filter-phase + (name "filter") + (phase "helo") + (decision "bypass") + (options + (list + (opensmtpd-option + (option "fcrdns") + (data (opensmtpd-table + (name "table") + (data (list "hello" "cat"))))))))) + + (test-error "Test must have at least one option." + #t + (opensmtpd-filter-phase + (name "filter") + (phase "helo") + (decision "bypass"))) + + (test-error + "(filter-phase (options ...)) must be a list of s." + #t + (opensmtpd-filter-phase + (name "rdns") + (decision "junk") + (phase "helo") + (options + (list 5)))) + + (test-error "Test (decision \"junks\") is invalid." + #t + (opensmtpd-filter-phase + (name "this") + (options (list + (opensmtpd-option + (option "auth")))) + (phase "helo") + (decision "junks"))) + + (test-error + (string-append + "(local-delivery (virtual ...) must be an " + "whose 'data' is an alist.") + #t + (opensmtpd-local-delivery + (name "receive") + (method (opensmtpd-maildir + (pathname "/home/%{rcpt.user}/Maildir"))) + (virtual (opensmtpd-table + (name "virt") + (data (list "jbranso@dismail.de")))))) + + (test-error "(opensmtpd-match (options ...)) should not be quoted." + #t + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options + '((opensmtpd-option + (option "for any")))))) + + (test-error "Test has duplicate 'for' options." + #t + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options (list + (opensmtpd-option + (option "for any")) + (opensmtpd-option + (option "for local")))))) + + (test-error "Test has duplicate 'from' options." + #t + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options (list + (opensmtpd-option + (option "from any")) + (opensmtpd-option + (option "from auth")))))) + + (define option-string1 + (list-ref (list "helo" "rcpt-to" "mail-from") + (random 3))) + ;; rcpt-to, mail-from, and helo must have a data field. + (test-error (string-append "Test (opensmtpd-option (option \"" + option-string1 + "\")) must also define fieldname 'data'.") + #t + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options (list + (opensmtpd-option + (option option-string1)))))) + (define option-string2 + (list-ref (list "for local" "for any" "from any" + "from local" "from socket" "tls") + (random 6))) + ;; "for local" "for any" "from any" "from local" "from socket" "tls" + ;; cannot have data or regex defined + (test-error (string-append "Test (opensmtpd-option (option \"" + option-string2 + "\")) cannot have fieldname 'data' defined.") + #t + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options (list + (opensmtpd-option + (option option-string2) + (regex #t)))))) + + (test-error "(opensmtpd-match (action ...)) needs to be defined." + #t + (opensmtpd-match + (options (list + (opensmtpd-option + (option "from auth")))))) + + (test-error "(opensmtpd-match (options ...)) has duplicate 'helo's." + #t + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options (list + (opensmtpd-option + (option "helo") + (bool #f)) + (opensmtpd-option + (option "helo")))))) + + (test-error "(opensmtpd-match (options ...)) has duplicate 'mail-from's." + #t + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options (list + (opensmtpd-option + (option "mail-from") + (data "hello")) + (opensmtpd-option + (option "mail-from") + (data "world")))))) + + (test-error + "(opensmtpd-match (options ...)) has an invalid option name: fcrdns." + #t + (opensmtpd-match + (options (list + (opensmtpd-option + (option "fcrdns")))) + (action (opensmtpd-relay + (name "relay"))))) + + (test-error + "(opensmtpd-match (options ...)) has an invalid option name: rdns." + #t + (opensmtpd-match + (options (list + (opensmtpd-option + (option "rdns")))) + (action (opensmtpd-relay + (name "relay"))))) + + (test-error + (string-append + "(opensmtpd-match (options ...)) option-name 'tag' must " + "also have 'data' be a string.") + #t + (opensmtpd-match + (options (list + (opensmtpd-option + (option "tag")))) + (action (opensmtpd-relay + (name "relay"))))) + + (define option-string3 + (list-ref (list "for domain" "for rcpt-to" + "from mail-from" "from src") + (random 4))) + ;; the options in this list: + ;; (list "for domain" "for rcpt-to" "from mail-from" "from src") + ;; must have a data field. + (test-error + (string-append "Test (opensmtpd-option (option \"" + option-string3 "\"))" + " must define fieldname 'data'.") + #t + (opensmtpd-match + (options (list + (opensmtpd-option + (option option-string3)))) + (action (opensmtpd-relay + (name "relay"))))) + + (define option-string4 + (list-ref (list "for local" "for any" "from any" + "from local" "from socket" "tls") + (random 6))) + ;; the options in this list cannot have a data or regex field defined. + ;; (list "for local" "for any" "from any" "from local" "from socket" "tls") + (test-error (string-append "Test (opensmtpd-option (option \"" + option-string4 "\"))" + " cannot define fieldname 'data'.") + #t + (opensmtpd-match + (options (list + (opensmtpd-option + (regex #t) + (option option-string4)))) + (action (opensmtpd-relay + (name "relay"))))) + + + (define option-string5 + (list-ref (list "for domain" "for rcpt-to" + "from mail-from" "from src") + (random 4))) + (test-error (string-append "Test (opensmtpd-option (option \"" + option-string5 "\"))" + " must define fieldname 'data' as a string or " + "an , whose 'data' \n" + "is a list of strings.") + #t + (opensmtpd-match + (options + (list (opensmtpd-option + (option option-string5) + (data (opensmtpd-table + (name "src-table") + (data '(("127.0.0.1" . "374.394.405.23")))))))) + (action (opensmtpd-relay + (name "relay"))))) + + ;; match must have at least one option. + (test-error + "(opensmtpd-match (options ...)) must have at least one ." + #t + (opensmtpd-match + (action + (opensmtpd-local-delivery + (name "mail"))))) + + ;; you cannot have strings of length 0. + (test-error + (string-append + "(opensmtpd-table (name \"table\") (data '((\"james\" . \"\")))) " + "has an empty string.") + #t + (opensmtpd-table + (name "mytable") + (data '(("hello" . ""))))) + + (define good-match1 + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options (list (opensmtpd-option + (option "for any")) + (opensmtpd-option + (option "from any")) + (opensmtpd-option + (option "auth")))))) + + (test-assert "good match" (test-good-record good-match1)) + ;;(test-assert "Test is valid.") + + ;; I used to have some code that assumed if you have + ;; an "" and an "" using the same filter, then that + ;; was a misconfiguration. It's not. + (define (good-opensmtpd-configuration1) + (let ([interface "lo"] + [filter-dkimsign + (opensmtpd-filter + (name "dkimsign") + (exec #t) + (proc (list (file-append opensmtpd-filter-dkimsign + "/libexec/opensmtpd/filter-dkimsign") + " -d gnucode.me -s 2021-09-22 -c relaxed/relaxed -k " + "/etc/dkim/private.key " + "user nobody group nogroup")))]) + (opensmtpd-configuration + (interfaces + (list + ;; send out emails and be sure to dkimsign them. + (opensmtpd-interface + (interface interface) + (filters (list filter-dkimsign))))) + (socket + (opensmtpd-socket + (filters (list filter-dkimsign)))) + (matches (list + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options (list (opensmtpd-option + (option "for any")) + (opensmtpd-option + (option "from any")) + (opensmtpd-option + (option "auth")))))))))) + + (test-assert + (string-append "opensmtpd-configuration may use the same dkimsign " + "filter on and .") + (test-good-record good-opensmtpd-configuration1)) + + ;; this is just the largest configuration that I can test. + (define (good-opensmtpd-configuration2) + (let ([interface "lo"] + [creds-table + (opensmtpd-table + (name "creds") + (data + (list + (cons "joshua" + "$6$Ec4m8FgKjT2F/03Y$k66ABdse9TzCX6qaALB3WBL9GC1rmAWJmaoSjFMpbhzat7DOpFqpnOwpbZ34wwsQYIK8RQlqwM1I/v6vsRq86."))))] + [receive-action + (opensmtpd-local-delivery + (name "receive") + (method (opensmtpd-maildir + (pathname "/home/%{rcpt.user}/Maildir") + (junk #t))) + (alias (opensmtpd-table + (name "aliases") + (data '(("joshua@gnucode.me" . "joshua"))))) + (virtual (opensmtpd-table + (name "virt") + (data '(("josh" + . "jbranso@dismail.de"))))))] + ;; as of 7-24-22 this proc fieldname does not actually work, but + ;; is proper syntax. + [filter-dkimsign (opensmtpd-filter + (name "dkimsign") + (exec #t) + (proc (list + (file-append + opensmtpd-filter-dkimsign + "/libexec/opensmtpd/filter-dkimsign") + " -d gnucode.me -s 2021-09-22 -c " + "relaxed/relaxed -k " + "/etc/dkim/private.key " + "user nobody group nogroup")))] + [filter-invalid-fcrdns (opensmtpd-filter-phase + (name "invalid-fcrdns") + (phase "connect") + (options + (list (opensmtpd-option + (option "fcrdns") + (bool #f)))) + (decision "reject") + (message "422 No valid fcrdns."))] + [filter-invalid-rdns (opensmtpd-filter-phase + (name "invalid-rdns") + (phase "connect") + (options + (list (opensmtpd-option + (option "rdns") + (bool #f)))) + (decision "junk"))] + [smtp.gnucode.me (opensmtpd-pki + (domain "smtp.gnucode.me") + (cert "guix.scm") + (key "guix.scm"))]) + (opensmtpd-configuration + (mta-max-deferred 50) + (queue + (opensmtpd-queue + (compression #t))) + (smtp + (opensmtpd-smtp + (max-message-size "10M"))) + (srs + (opensmtpd-srs + (ttl-delay "5d"))) + (interfaces + (list + (opensmtpd-interface + (interface interface) + (port 25) + (secure-connection "tls") + (filters (list filter-invalid-fcrdns + filter-invalid-rdns)) + (pki smtp.gnucode.me)) + ;; this lets local users logged into the system via ssh send email + ;; be sure to dkimsign them. + (opensmtpd-interface + (interface interface) + (port 465) + (secure-connection "smtps") + (pki smtp.gnucode.me) + (auth creds-table) + (filters (list filter-dkimsign))) + ;; if you uncomment this next line, then you get issues. + ;;(opensmtpd-socket + ;; (filters (list filter-dkimsign))) + ;; send out emails and be sure to dkimsign them. + (opensmtpd-interface + (interface interface) + (port 587) + (secure-connection "tls-require") + (pki smtp.gnucode.me) + (auth creds-table) + (filters (list filter-dkimsign))))) + (socket + (opensmtpd-socket + (filters (list filter-dkimsign)) + (tag "socket"))) + (matches (list + (opensmtpd-match + (action (opensmtpd-relay + (name "relay"))) + (options (list (opensmtpd-option + (option "for any")) + (opensmtpd-option + (option "from any")) + (opensmtpd-option + (option "auth"))))) + (opensmtpd-match + (action receive-action) + (options (list (opensmtpd-option + (option "from any")) + (opensmtpd-option + (option "for domain") + (data (opensmtpd-table + (name "domain-table") + (data (list "gnucode.me" + "gnu-hurd.com")))))))) + (opensmtpd-match + (action receive-action) + (options (list (opensmtpd-option + (option "for local")))))))))) + + (test-assert "Test my largish example ." + (test-good-record good-opensmtpd-configuration2)) + + ;; the matches have two actions with the same name, + ;; but are different actions. + (test-error + (string-append "Test fieldname 'matches' has " + "two actions with the same name, but the actions are " + "different.") + #t + (opensmtpd-configuration + (matches + (list (opensmtpd-match + (options + (list + (opensmtpd-option + (option "auth")))) + (action + (opensmtpd-local-delivery + (name "my-local-delivery") + (ttl "50m")))) + (opensmtpd-match + (options + (list + (opensmtpd-option + (option "auth")))) + (action + (opensmtpd-local-delivery + (name "my-local-delivery") + (ttl "50h")))))))) + + ;; you can only have 1 opensmtpd-socket. + (test-error + (string-append + "(opensmtpd-configuration> (listen-on ...)) may only have " + "one .") + #t + (let ([interface "lo"]) + (opensmtpd-configuration + (socket + (list + (opensmtpd-socket) + (opensmtpd-socket))) + (matches (list + (opensmtpd-match + (options + (list + (opensmtpd-option + (option "auth")))) + (action (opensmtpd-relay + (name "relay"))))))))) + + (test-end "run-opensmtpd-record-sanitation-test")) + +(define %test-opensmtpd-record-sanitation + (system-test + (name "opensmtpdRecordSanitation") + (description + (string-append " has numerous sanity checks.\n" + "This checks that invalid configurations, return an\n" + "appropriate error.\n")) + (value (run-opensmtpd-record-sanitation-test)))) + (define %exim-os (simple-operating-system base-commit: 4b3493ed0156709a924f31ef4c9a5efa0815dfe8 -- 2.38.1 From debbugs-submit-bounces@debbugs.gnu.org Mon Dec 26 14:35:01 2022 Received: (at 56046) by debbugs.gnu.org; 26 Dec 2022 19:35:01 +0000 Received: from localhost ([127.0.0.1]:54129 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1p9tFR-00088T-RS for submit@debbugs.gnu.org; Mon, 26 Dec 2022 14:35:01 -0500 Received: from mail-wm1-f65.google.com ([209.85.128.65]:35733) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1p9tFN-00088C-20 for 56046@debbugs.gnu.org; Mon, 26 Dec 2022 14:34:56 -0500 Received: by mail-wm1-f65.google.com with SMTP id m8-20020a05600c3b0800b003d96f801c48so5576657wms.0 for <56046@debbugs.gnu.org>; Mon, 26 Dec 2022 11:34:53 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=mime-version:user-agent:content-transfer-encoding:references :in-reply-to:date:cc:to:from:subject:message-id:from:to:cc:subject :date:message-id:reply-to; bh=DH0uGBkVsQH/eD6sIv4g13Jybt+/1AdDUWlfScwaWpE=; b=h7qhWGPdCO/u29qMGL86AubeBykdbmQUfAWQNP4Fe4sXhvuwzvY9yc0xUoKPcHJ0gg IsSV9MbwRNG7wkL9kHSKGZlOKeIv9MXHSPn1KDDjvL+AkNQvgnOtTGGg8NLobdz3q2TG JnK0TI4fg7AarDP8263SeXt0ZfgBmTNTQtaMkR761kbJu5rYJYcyMPYwBSqc3Bc6l/g4 SD1EDjOqZxVnhqZ0dun7uP2U5uDebAL+syRmxiL+5/MSHZLdlcoVLVY4JeFu3Hz/HEwD H0bPOpYdNAB9rtP8BTGzltD69qP/22I5wf9VkSM2I1PgSwc9uqaWjlDDXNRBuX4ehZrN oLqA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=mime-version:user-agent:content-transfer-encoding:references :in-reply-to:date:cc:to:from:subject:message-id:x-gm-message-state :from:to:cc:subject:date:message-id:reply-to; bh=DH0uGBkVsQH/eD6sIv4g13Jybt+/1AdDUWlfScwaWpE=; b=nR1C5Ix0sM0URtMwPVQ9wbMBjfPfrGoTHUrw31os4Ein3rJjC2LFuHvaC1p5lF0Huy XLSPLWXLPfnyktTP83xly6e2xH+nRMFxKzSKGpCtLAH6NTpH8O2QSaLNRYiI+J8fT9am 1Ope9rIkcmtAIm10s3fN2DL38HzNWYQtNjd3khYRejrg0LpKfRHLq6L/qXVnmIq/XCoo +8TBXME9Ddz8QLMjzZnsN0VwDTPAy3auK8ij+v0OF6qbM5HNKOmvAEUdDl32F4Ox5jo6 I2H9v3d1sog5VEtWKMn2uz4/8cE4gzHfVptgVI1prOnc6wDBLzA91UjuBDWlaAVcQovz slKA== X-Gm-Message-State: AFqh2kq4VOze60l1hgAcYaPtRBWRKrVK8PBeePcXAZnx2B09hTHqbj22 WjZtKZwx0zFFK16dk1qU2cA= X-Google-Smtp-Source: AMrXdXtGyrB3K+cYeHg98skf1T2t9hQe3SHU98n8wDnawZKDVkr27Z+Fw88Rpqp0G3PenCXBLLjqjA== X-Received: by 2002:a05:600c:3b90:b0:3d1:f0f1:ceb4 with SMTP id n16-20020a05600c3b9000b003d1f0f1ceb4mr13693938wms.19.1672083286153; Mon, 26 Dec 2022 11:34:46 -0800 (PST) Received: from lumine.fritz.box (85-127-52-93.dsl.dynamic.surfer.at. [85.127.52.93]) by smtp.gmail.com with ESMTPSA id d22-20020a1c7316000000b003d9862ec435sm2286156wmb.20.2022.12.26.11.34.45 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 26 Dec 2022 11:34:45 -0800 (PST) Message-ID: <37239abae388f1fadcba559b62c0aae07de9d319.camel@gmail.com> Subject: Re: [PATCH opensmtpd-records v3] services (opensmtpd): add opensmtpd records to enhance opensmtpd-configuration. From: Liliana Marie Prikler To: Joshua Branson , 56046@debbugs.gnu.org Date: Mon, 26 Dec 2022 20:34:43 +0100 In-Reply-To: <8001f404c0e54baba71ef9c5536abc54275c4167.1671803496.git.joshua@gnucode.me> References: <8001f404c0e54baba71ef9c5536abc54275c4167.1671803496.git.joshua@gnucode.me> Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable User-Agent: Evolution 3.46.0 MIME-Version: 1.0 X-Spam-Score: 0.0 (/) X-Debbugs-Envelope-To: 56046 Cc: Joshua Branson X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) Am Freitag, dem 23.12.2022 um 08:52 -0500 schrieb Joshua Branson: >=20 > * gnu/services/mail.scm: > (opensmtpd-table, > opensmtpd-ca, > opensmtpd-pki, > opensmtpd-action-local-delivery, > opensmtpd-maildir, > opensmtpd-mda, > opensmtpd-lmtp, > opensmtpd-relay, > opensmtpd-option, > opensmtpd-filter-phase, > opensmtpd-filter, > opensmtpd-interface, > opensmtpd-socket, > opensmtpd-match, > opensmtpd-smtp, > opensmtpd-srs, > opensmtpd-queue, and > opensmtpd-configuration): New records. Don't forget to put closing parentheses at the end of lines. Also, feel free to group them to save vertical space. > (false?, is-value-right-type, add-comma-or-string, > list-of-procedures->string, string-in-list?, my-sanitize, > opensmtpd-filter-chain?, throw-error-duplicate-option, > sanitize-list-of-options-for-match, sanitize-filters, > list-has-duplicates-or-non-filters?, > filter-phase-has-message-and-value?, > filter-phase-decision-lacks-proper-message?, > filter-phase-lacks-proper-value?, > filter-phase-has-incorrect-junk-or-bypass?, > filter-phase-junks-after-commit?, > list-of-unique-filter-or-filter-phase?, throw-error, > contains-duplicate?, list-of-type?, list-of-strings?, > list-of-unique-opensmtpd-option?, > list-of-opensmtpd-ca?, > list-of-opensmtpd-pki?, > list-of-opensmtpd-listen-on?, > list-of-unique-opensmtpd-match?, list-of-strings->string, > assoc-list? assoc-list, variable->string, > tables-data-are-assoc-list?, > tables-data-are-a-list-of-strings?, > table-data-are-a-nested-list-of-strings?, > assoc-list->string, > opensmtpd-table->string, > opensmtpd-listen-on->string, > opensmtpd-listen-on-socket->string, > opensmtpd-action-relay->string, > opensmtpd-lmtp->string, > opensmtpd-mda->string, > opensmtpd-maildir->string, > opensmtpd-action-local-delivery->string, > opensmtpd-action->string, opensmtpd-option->string, > opensmtpd-match->string, > opensmtpd-ca->string, opensmtpd-pki->string, > generate-filter-chain-name, opensmtpd-filter-chain->string, > opensmtpd-filter-phase->string, opensmtpd-filters->string, > opensmtpd-listen->string, > opensmtpd-srs->string, > opensmtpd-smtp->string, > opensmtpd-queue->string, get-opensmtpd-actions, > get-opensmtpd-pkis, get-opensmtpd-filters, flatten, > get-opensmtpd-tables, opensmtpd-fieldname->string, > list-of-records->string, opensmtpd->mixed-text-file): New > procedures. >=20 > * gnu/tests/mail.scm : new tests for various opensmtpd records. >=20 > * doc/guix.texi (OpenSMTPD Service): Added documentation for the > new records for opensmtpd. > --- > =C2=A0doc/guix.texi=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 | 106= 5 ++++++++++++++++- > =C2=A0gnu/services/mail.scm | 2560 > ++++++++++++++++++++++++++++++++++++++++- > =C2=A0gnu/tests/mail.scm=C2=A0=C2=A0=C2=A0 |=C2=A0 713 ++++++++++++ > =C2=A03 files changed, 4310 insertions(+), 28 deletions(-) >=20 > diff --git a/doc/guix.texi b/doc/guix.texi > index 535c8cdfc3..879a2ad233 100644 > --- a/doc/guix.texi > +++ b/doc/guix.texi > @@ -25407,16 +25407,66 @@ could instantiate a dovecot service like > this: > =C2=A0@end lisp > =C2=A0 > =C2=A0@subsubheading OpenSMTPD Service > +@cindex opensmtpd > =C2=A0 > =C2=A0@deffn {Scheme Variable} opensmtpd-service-type > -This is the type of the @uref{https://www.opensmtpd.org, OpenSMTPD} > -service, whose value should be an @code{opensmtpd-configuration} > object > -as in this example: > +OpenSMTPD is an easy-to-use mail transfer agent (MTA).=C2=A0 OpenSMTPD > +@strong{listens} for incoming mail and @strong{matches} the mail to > +@strong{actions}. The following records represent those stages: > =C2=A0 > -@lisp > -(service opensmtpd-service-type > -=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-configuratio= n > -=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (config-fil= e (local-file "./my-smtpd.conf")))) > +@multitable {aaaaaaaaa} > {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} I hope this will receive proper documentation. > +@item @strong{listens} > +@tab @code{} > +@item > +@tab @code{} > +@item > +@tab > +@item @strong{matches} > +@tab @code{} > +@item > +@tab > +@item @strong{actions} > +@tab @code{} > +@item > +@tab @code{} > +@end multitable > + > +Additionally, each @code{} and > +@code{} may use a list of @code{ filter>}, > +and/or @code{} records to filter > +email/spam. Also numerous records' fieldnames use > +@code{} records to hold lists or key value pairs of > +data.=C2=A0 Be sure to read the @code{} section to lear= n > the > +differance between a @code{mapping table} and a @code{list table}. > + > +Finally, both @code{} and > +@code{} use @code{} to > +configure various options. > + > +A simple example opensmtpd configuration is below: > + > +@lisp > +(let ((smtp.gnu.org (opensmtpd-pki > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (domain = "smtp.gnu.org") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (cert "f= ile.cert") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (key "fi= le.key")))) > +=C2=A0 (service opensmtpd-service-type > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-= configuration > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (inte= rfaces (list > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (o= pensmtpd-interface > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0 (pki smtp.gnu.org)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (o= pensmtpd-interface > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0 (pki smtp.gnu.org) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0 (secure-connection "smtps")))) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (matc= hes (list > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-match > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (action > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmt= pd-local-delivery > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (n= ame "local-delivery")))) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-match > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (action > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmt= pd-relay > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (n= ame "relay"))))))))) > =C2=A0@end lisp > =C2=A0@end deffn > =C2=A0 > @@ -25425,7 +25475,7 @@ Data type representing the configuration of > opensmtpd. > =C2=A0 > =C2=A0@table @asis > =C2=A0@item @code{package} (default: @var{opensmtpd}) > -Package object of the OpenSMTPD SMTP server. > +Package object of the OpenSMTPD server. > =C2=A0 > =C2=A0@item @code{config-file} (default: @code{%default-opensmtpd-config- > file}) > =C2=A0File-like object of the OpenSMTPD configuration file to use.=C2=A0 = By > default > @@ -25433,14 +25483,1013 @@ it listens on the loopback network > interface, and allows for mail from > =C2=A0users and daemons on the local machine, as well as permitting email > to > =C2=A0remote servers.=C2=A0 Run @command{man smtpd.conf} for more informa= tion. > =C2=A0 > +@item @code{bounce} (default: @code{(list "4h")}) > +@code{bounce} is a list of strings, which send warning messages to > the > +envelope sender when temporary delivery failures cause a message to > +remain in the queue for longer than a specified delay. Each delay > option > +is a string parameter beginning with a positive decimal integer and > a > +unit, which can be 's', 'm', 'h', or 'd'. At most four delay > parameters > +can be specified. > + > +@item @code{interfaces} default: > +@lisp > +(list > +=C2=A0 (opensmtpd-interface > +=C2=A0=C2=A0=C2=A0 (interface "lo") > +=C2=A0=C2=A0=C2=A0 (port 25))) > +@end lisp > +@code{interfaces} is a list of @code{} records. > +This list details what interfaces and ports OpenSMTPD listens on as > well as > +other options. > + > +@item @code{socket} (default: @code{(opensmtpd-socket)}) > +Listens for incoming connections on the Unix domain socket. > + > +@item @code{includes} (default: @code{#f}) > +@code{includes} is a list of string filenames. Each filename's > contents is > +additional configuration that is inserted into the top of the > configuration > +file.=C2=A0 Run @code{man smtpd.conf} for more information. > + > +@item @code{matches} default: > +@lisp > +(list (opensmtpd-match > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (action (opensmtpd-local-delivery > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0 (name "local") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0 (method "mbox") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0 (options > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0 (list > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-option > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (option "for local"))))))) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-match > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (action (opensmtpd-relay > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0 (name "outbound"))) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (options > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (list > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-option > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (option "from loc= al")) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-option > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (option "for any"= )))))) > +@end lisp > +@code{matches} is a list of @code{} records, which > +matches incoming mail and sends it to a correspending action. The > match > +records are evaluated sequentially, with the first match winning. > +Therefore @emph{the order that you arrange your matches is > important}. > +It's a good idea to put specific matches first and an all > emcompassing > +match (like @code{(option "for any")}) @strong{last}. If an incoming > +mail does not match any match records, then it is rejected. > + > +@item @code{mta-max-deferred} (default: @code{100}) > +When delivery to a given host is suspended due to temporary > failures, cache > +at most number envelopes for that host such that they can be > delivered as > +soon as another delivery succeeds to that host. The default is 100. > + > +@item @code{queue} (default: @code{#f}) > +@code{queue} expects an @code{} record. With it, > one may > +compress and encrypt queue-ed emails as well as set the default > expiration > +time for temporarily undeliverable messages. > + > +@item @code{smtp} (default: @code{#f}) > +@code{smtp} expects an @code{} record, which lets > one > +specifiy how large email may be along with other settings. > + > +@item @code{srs} (default: @code{#f}) > +@code{srs} expects an @code{} record, which lets one > set > +up SRS, the Sender Rewritting Scheme. > + > =C2=A0@item @code{setgid-commands?} (default: @code{#t}) > =C2=A0Make the following commands setgid to @code{smtpq} so they can be > =C2=A0executed: @command{smtpctl}, @command{sendmail}, @command{send- > mail}, > =C2=A0@command{makemap}, @command{mailq}, and @command{newaliases}. > =C2=A0@xref{Setuid Programs}, for more information on setgid programs. > + > =C2=A0@end table > =C2=A0@end deftp > =C2=A0 > +@itemize > +@item Data Type: opensmtpd-interface > +Data type representing the configuration of an > +@code{}. It listens on the fieldname > +@code{interface} for incoming connections, using the same syntax as > +@code{ifconfig}. The interface parameter may also be an string > interface > +group, an string IP address, or a string domain name. Listening can > +optionally be restricted to a specific address via the fieldname > +@code{family}, which can be either @code{"inet4"} or @code{"inet6"}. > + > +@itemize > +@item @code{interface} (default: @code{"lo"}) > + > +The string interface to listen for incoming connections.=C2=A0 This > string > +may be an interface group, an IP address, or a domain name. These > +interfaces can usually be found by the command @code{ip link}. > + > +@item @code{family} (default: @code{#f}) > + > +Only listen on a specific address family.=C2=A0 Valid strings are > +@code{"inet4"} or @code{"inet6"}, which will only listen on IPv4 or > IPv6 > +respectfully.=C2=A0 If @code{(family #f)}, then opensmtpd will listen on > both > +IPv4 and IPv6. > + > +@item @code{auth} (default: @code{#f}) > +Support SMTPAUTH: clients may only start SMTP transactions after > +successful authentication. If @code{auth} is @code{#t}, then users > are > +authenticated against their own normal login credentials. > Alternatively > +@code{auth} may be a @code{mapping table} that maps usernames to > +encrypted passwords.=C2=A0 The password can be encrypted via the > +@code{smtpctl} @code{encrypt} subcommand. > + > +@item @code{auth-optional} (default: @code{#f}) > +Support SMTPAUTH optionally: clients need not authenticate, but may > do > +so.=C2=A0 This allows the @code{} to both accept > +incoming mail from untrusted senders and permit outgoing mail from > +authenticated users. It can be used in situations where it is not > +possible to listen on a separate port (usually the submission port, > 587) > +for users to authenticate.=C2=A0 This option also accepts a @code{mappin= g > +table} that maps usernames to encrypted passwords. > + > +@item @code{filters} (default: @code{#f}) > +A list of one or many @code{} or > +@code{} records. The filters are applied > +sequentially. These records listen and filter on connections handled > by this > +listener. > + > +@item @code{hostname} (default: @code{#f}) > +Change the default server name in the greeting banner instead of the > +default one. > + > +@item @code{hostnames} (default: @code{#f}) > +Override the server name for specific addresses. Use a @code{mapping > +table} that maps string IP addresses to string hostnames. If the > address > +on which the connection arrives appears in the mapping, the > associated > +hostname is used. > + > +@item @code{mask-src} (default: @code{#f}) > +If @code{#t}, then omit the from part when prepending =E2=80=9CReceived= =E2=80=9D > headers. > + > +@item @code{disable-dsn} (default: @code{#f}) > +When @code{#t}, then disable the DSN (Delivery Status Notification) > extension. > + > +@item @code{pki} (default: @code{#f}) > +For secure connections, use an @code{} record to > prove a > +mail server's identity. > + > +@item @code{port} (default: @code{25}) > +Listen on the integer port instead of the default port of 25. > + > +@item @code{proxy-v2} (default: @code{#f}) > +If @code{#t}, then support the PROXYv2 protocol, rewriting > appropriately source > +address received from proxy. > + > +@item @code{received-auth} (default: @code{#f}) > +If @code{#t}, then in =E2=80=9CReceived=E2=80=9D headers, report whether= the session > was > +authenticated and by which local user. > + > +@item @code{senders} (default: @code{#f}) > +Look up the authenticated user in the supplied @code{mapping table} > to > +find the email addresses that user is allowed to submit mail as. > + > +@item @code{masquerade} (default: @code{#f}) > +@code{masquerade}, is used in conjunction with @code{senders}.=C2=A0 If > +@code{#t}, then the From header is rewritten to match the sender > +provided in the SMTP session.=C2=A0 If @code{senders} is @code{#false}, > then > +@code{masquerade} cannot be @code{#t}. > + > +@item @code{secure-connection} (default: @code{#f}) > +This is a string of one of these options: > + > +@multitable {aaaaaaaaaaaaaaaaaaaa} > {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} Same here > +@item @code{"smtps"} > +@tab Support SMTPS, by default on port 465. > +@item @code{"tls"} > +@tab Support STARTTLS, by default on port 25. > +@item @code{"tls-require"} > +@tab Like @code{"tls"}, but force clients to > +@item > +@tab establish a secure connection before being > +@item > +@tab allowed to start an SMTP transaction. > +@item @code{"tls-require-verify"} > +@tab Like @code{"tls-require"}, but clients must > +@item > +@tab also provide a valid certificate > +@item > +@tab to establish an SMTP session. > +@end multitable > + > +@item @code{tag} (default: @code{#f}) > +Clients connecting to the listener are tagged with the given string > tag. > +@end itemize > + > +@item Data Type: opensmtpd-socket > +Data type representing the configuration of an > +@code{}. Listen for incoming SMTP connections on > the > +Unix domain socket @samp{/var/run/smtpd.sock}. This is done by > default, > +even if the record is absent. > + > +@itemize > +@item @code{filters} (default: @code{#f}) > +A list of one or many @code{} or > +@code{} records. These filter incoming > +connections handled by this listener. > + > +@item @code{mask-src} (default: @code{#f}) > +If @code{#t}, then omit the from part when prepending =E2=80=9CReceived= =E2=80=9D > headers. > + > +@item @code{tag} (default: @code{#f}) > +Clients connecting to the listener are tagged with the given string > tag. > +@end itemize > + > +@item Data Type: opensmtpd-match > +@cindex opensmtpd-match > +This data type represents the configuration of an > +@code{} record. > + > +If at least one mail envelope matches the options of one match > record, > +receive the incoming message, put a copy into each matching > envelope, > +and atomically save the envelopes to the mail spool for later > processing > +by the respective @code{} found in fieldname > +@code{action}.=C2=A0 Here is an example @code{opensmtpd-match} > +record. > + > +@lisp > +(opensmtpd-match > + (action (opensmtpd-local-delivery > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (name "receive") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (method (opensmtp= d-maildir > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (pathname "/home/%@{rcpt.user@}/Maildi= r") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (junk #t))) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (virtual (opensmt= pd-table > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (name "virt") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (data '(("carmen" . "carmen@@gnu= .org"))))))) > + (options (list (opensmtpd-option > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0 (option "from any")) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0 (opensmtpd-option > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0 (option "for domain") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0 (data (opensmtpd-table > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (name "d= omain-table") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (data (l= ist "gnu.org" "fsf.org")))))))) > +@end lisp > + > +@itemize > +@item @code{action} (default: @code{#f}) > + > +If mail matches this match configuration, then do this action. Valid > values > +include @code{} or > +@code{}. > + > +@item @code{options} (default: @code{#f}) > +The fieldname @code{option} is a list of unique > +@code{} records. > + > +There are some mutually exclusive options: there can be only one > ``for'' > +and only one ``from'' option. > + > +@multitable {aaaaaaaaaaaaaaaaaaaaaaaaa} > {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} and here. > +@headitem for > +@tab from > +@item only use one of the following: > +@tab only use one of the following: > +@item @code{"for any"} > +@tab @code{"from any"} > +@item @code{"for local"} > +@tab @code{"from auth"} > +@item @code{"for domain"} > +@tab @code{"from local"} > +@item @code{"for rcpt-to"} > +@tab @code{"from mail-from"} > +@item > +@tab @code{"from socket"} > +@item > +@tab @code{"from src"} > +@end multitable > + > +Additionally, some options require additional data via > +@code{}'s fieldname @code{data}.=C2=A0 The following > list > +will explain the below syntax. > + > +@itemize > +@item @code{"for any"} > +This option only requires fieldname @code{option} to have the string > +@code{"for any"}: > + > +@lisp > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-option > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (option "for any")) > +@end lisp > + > +@item @code{"tag"} _tag_ > +This option only requires fieldname @code{option} to have the string > +@code{"tag"} with a string in fieldname @code{data}: > + > +@lisp > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-option > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (option "tag") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (data "this-tag")) > +@end lisp > + > +@item @code{"for rcpt"} _domain_ | > +This option requires fieldname @code{data} to have a string domain > or > +@code{list table}: > + > +@lisp > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-option > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (option "for rcpt") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (data "gnu.org")) > +@end lisp > + > +OR > + > +@lisp > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-option > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (option "for rcpt") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (data (list "gnu.org" "fsf.org"))) > +@end lisp > +@end itemize > + > +The following matching options are supported and can all be negated > (via not > +#t). The options that support a table (anything surrounded with '<' > and '>' > +eg:
), also support specifying regex via (regex #t). > + > +@itemize > +@item @code{"for any"} > +Specify that session may address any destination. > + > +@item @code{"for local"} > +Specify that session may address any local domain.=C2=A0 This is the > default, > +and may be omitted. > + > +@item @code{"for domain"} _domain_ | > +Specify that session may address the string _domain_ or > +@code{list table} . > + > +@item @code{"for rcpt-to"} _recipient_ | > +Specify that session may address the string _recipient_ or list > table > +. > + > +@item @code{"from any"} > +Specify that session may originate from any source. > + > +@item @code{"from auth"} > +Specify that session may originate from any authenticated user, no > matter > +the source IP address. > + > +@item @code{"from auth"} _user_ | > +Specify that the session may originate from authenticated _user_ or > +@code{list table} , no matter the source IP address. > + > +@item @code{"from local"} > +Specify that session may only originate from a local IP address, or > from > +the local enqueuer.=C2=A0 This is the default, and may be omitted. > + > +@item @code{"from mail-from"} _sender_ | > +Specify that session may originate from _sender_ or @code{list > table} > +, no matter the source IP address. > + > +@item @code{"from rdns"} > +Specify that session may only originate from an IP address that > resolves > +to a reverse DNS@. > + > +@item @samp{"from rdns"} _hostname_ | > +Specify that session may only originate from an IP address that > resolves > +to a reverse DNS matching string _hostname_ or @code{list table} > +. > + > +@item @samp{"from socket"} > +Specify that session may only originate from the local enqueuer. > + > +@item @code{"from src"} _address_ |
> +Specify that session may only originate from string _address_ or > +@code{list table}
which can be a specific address or a > subnet > +expressed in CIDR-notation. > + > +@item @code{"auth"} > +Matches transactions which have been authenticated. > + > +@item @code{"auth"} _username_ | > +Matches transactions which have been authenticated for string _user_ > or > +@code{list table} . > + > +@item @code{"helo"} _helo-name_ | > +Specify that session's HELO / EHLO should match the string _helo- > name_ > +or @code{list table} . > + > +@item @code{"mail-from"} _sender_ | > +Specify that transactions's MAIL FROM should match the string > _sender_ > +or @code{list table} . > + > +@item @code{"rcpt-to"} _recipient_ | > +Specify that transaction's RCPT TO should match the string > _recipient_ > +or @code{list table} . > + > +@item @code{"tag"} _tag_ > +Matches transactions tagged with the given tag. > + > +@item @code{"tls"} > +Specify that transaction should take place in a TLS channel. > +@end itemize > + > +@end itemize > + > +@item Data Type: opensmtpd-local-delivery > +This data type represents the configuration of an > +@code{} record. > + > +@itemize > +@item @code{name} (default: @code{#f}) > +@code{name} is the string name of the relay action. > + > +@item @code{method} (default: @code{"mbox"}) > +The email delivery option.=C2=A0 Valid options are: > + > +@itemize > +@item @code{"mbox"} > +Deliver the message to the user's mbox with mail.local(8). > + > +@item @code{"expand-only"} > +Only accept the message if a delivery method was specified in an > aliases > +or .forward file. > + > +@item @code{"forward-only"} > +Only accept the message if the recipient results in a remote address > after > +the processing of aliases or forward file. > + > +@item @code{} > +Deliver the message to an LMTP server at @code{}'s > +fieldname @code{destination}. The location may be expressed as > string > +host:port or as a UNIX socket. Optionally, @code{}'s > +fieldname @code{rcpt-to} might be specified to use the recipient > email > +address (after expansion) instead of the local user in the LMTP > session > +as RCPT TO. > + > +@item @code{} > +Deliver the message to the maildir in > +@code{}'s fieldname @code{pathname} if specified, > +or by default to @code{"~/Maildir"}. > + > +The pathname may contain format specifiers that are expanded before > use > +(see the below section about Format Specifiers). > + > +If @code{}'s record fieldname @code{junk} is > @code{#t}, > +then message will be moved to the =E2=80=98Junk=E2=80=99 folder if it co= ntains a > positive > +=E2=80=98X-Spam=E2=80=99 header. This folder will be created under field= name > @code{pathname} if > +it does not yet exist. > + > +@item @code{} > +Delegate the delivery to the @code{}'s fieldname > +@code{command} (type string) that receives the message on its > standard input. > + > +The @code{command} may contain format specifiers that are expanded > before use > +(see Format Specifiers). > +@end itemize > + > +@item @code{alias} (default: @code{#f}) > +Use the @code{mapping table} for aliases expansion. > + > +@item @code{ttl} (default: @code{#f}) > +@code{ttl} is a string specify how long a message may remain in the > queue.=C2=A0 It's > +format is @code{n@{s|m|h|d@}}.=C2=A0 eg: @code{"4m"} is four minutes. > + > +@item @code{user} (default: @code{#f} ) > +@code{user} is the string username for performing the delivery, to > be looked up > +with getpwnam(3). > + > +This is used for virtual hosting where a single username is in > charge of > +handling delivery for all virtual users. > + > +This option is not usable with the mbox delivery method. > + > +@item @code{userbase} (default: @code{#f}) > +@code{userbase} is an @code{} record for mapping > user > +lookups instead of the getpwnam(3) function. > + > +The fieldnames @code{user} and @code{userbase} are mutually > exclusive. > + > +@item @code{virtual} (default: @code{#f}) > +@code{virtual} is an @code{} record is used for > virtual > +expansion. > +@end itemize > + > +@item Data Type: opensmtpd-relay > +This data type represents the configuration of an > +@code{} record. > + > +@itemize > +@item @code{name} (default: @code{#f}) > +@code{name} is the string name of the relay action. > + > +@item @code{backup} (default: @code{#f}) > +When @code{#t}, operate as a backup mail exchanger delivering > messages to any > +mail exchanger with higher priority. > + > +@item @code{backup-mx} (default: @code{#f}) > +Operate as a backup mail exchanger delivering messages to any mail > exchanger > +with higher priority than mail exchanger identified as string name. > + > +@item @code{helo} (default: @code{#f}) > +Advertise string heloname as the hostname to other mail exchangers > during > +the HELO phase. > + > +@item @code{helo-src} (default: @code{#f} ) > + Use the mapping @code{} to look up a hostname > +matching the source address, to advertise during the HELO phase. > + > +@item @code{domain} (default: @code{#f}) > +Do not perform MX lookups but look up destination domain in an > +@code{} and use matching relay url as relay host. > + > +@item @code{host} (default: @code{#f}) > +Do not perform MX lookups but relay messages to the relay host > described by > +the string relay-url. The format for relay-url is > +@samp{[proto://[label@@]]host[:port]}. The following protocols are > available: > + > +@multitable {aaaaaaaaaa} > {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA. > +@item @code{smtp} > +@tab Normal SMTP session with opportunistic STARTTLS (the default). > +@item @code{smtp+tls} > +@tab Normal SMTP session with mandatory STARTTLS@. > +@item @code{smtp+notls} > +@tab Plain text SMTP session without TLS@. > +@item @code{lmtp} > +@tab LMTP session.=C2=A0 port is required. > +@item @code{smtps} > +@tab SMTP session with forced TLS on connection, default port is > +@item > +@tab 465. > +@end multitable > + > +Unless noted, port defaults to 25. > + > +The label corresponds to an entry in a credentials table, as > documented in > +@code{man table}. It is used with the @code{"smtp+tls"} and > @code{"smtps"} protocols for > +authentication. Server certificates for those protocols are verified > by > +default. > + > +@item @code{pki} (default: @code{#f}) > +For secure connections, use the certificate associated with > +@code{} (declared in a pki directive) to prove the > +client's identity to the remote mail server. > + > +@item @code{srs} (default: @code{#f}) > +If @code{#t}, then when relaying a mail resulting from a forward, > use the Sender > +Rewriting Scheme to rewrite sender address. > + > +@item @code{tls} (default: @code{#f}) boolean or string ``no- > verify'' Instead of a string, take 'no-verify as symbol perhaps? > +When @code{#t}, Require TLS to be used when relaying, using > mandatory STARTTLS by > +default. When used with a smarthost, the protocol must not be > +@samp{"smtp+notls://"}. When string @code{"no-verify"}, then do not > require a valid > +certificate. > + > +@item @code{auth} (default: @code{#f}) @code{} > +Use the alist @code{} for connecting to relay-url > +using credentials. This option is usable only with fieldname > @code{host} option. > + > +@item @code{mail-from} (default: @code{#f}) string > +Use the string mailaddress as MAIL FROM address within the SMTP > transaction. > + > +@item @code{src} (default: @code{#f}) string | @code{ table>} > +Use the string or @code{} sourceaddr for the > +source IP address, which is useful on machines with multiple > interfaces. If > +the list contains more than one address, all of them are used in > such a way > +that traffic is routed as efficiently as possible. > +@end itemize > + > +@item Data Type: opensmtpd-filter > +This data type represents the configuration of an > +@code{}. This is the filter record one should use > +if they want to use an external package to filter email eg: rspamd > or > +spamassassin. > + > +@itemize > +@item @code{name} (default: @code{#f}) > +The string name of the filter. > + > +@item @code{proc} (default: @code{#f}) > +The string command or process name.=C2=A0 If @code{proc-exec} is > @code{#t}, @code{proc} is > +treated as a command to execute.=C2=A0 Otherwise, it is a process name. > + > +@item @code{proc-exec} (default: @code{#f}) > +If @code{#t}, then execute the command in @code{proc}. > +@end itemize > + > +@item Data Type: opensmtpd-filter-phase > +This data type represents the configuration of an > +@code{}. > + > +In a regular workflow, @code{smtpd(8)} may accept or reject a > message > +based only on the content of envelopes. Its decisions are about the > +handling of the message, not about the handling of an active > session. > + > +Filtering extends the decision making process by allowing > +@code{smtpd(8)} to stop at each phase of an SMTP session, check that > +options are met, then decide if a session is allowed to move > forward. > + > +With filtering via an @code{} record, a > +session may be interrupted at any phase before an envelope is > complete. A > +message may also be rejected after being submitted, regardless of > whether the > +envelope was accepted or not. > + > +@itemize > +@item @code{name} (default: @code{#f}) > + > +The string name of the filter phase. > + > +@item @code{phase-name} (default: @code{#f}) > +The string name of the phase. Valid values are: > + > +@multitable {aaaaaaaaaaa} > {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} > +@item @code{"connect"} > +@tab upon connection, before a banner is displayed > +@item @code{"helo"} > +@tab after HELO command is submitted > +@item @code{"ehlo"} > +@tab after EHLO command is submitted > +@item @code{"mail-from"} > +@tab after MAIL FROM command is submitted > +@item @code{"rcpt-to"} > +@tab after RCPT TO command is submitted > +@item @code{"data"} > +@tab after DATA command is submitted > +@item @code{"commit"} > +@tab after message is fully is submitted > +@end multitable > + > +@item @code{options} (default @code{#f}) > +A list of unique @code{} records. > + > +At each phase, various options, specified by a list of > +@code{}, may be checked. The > +@code{}'s fieldname @code{option} values of: > +@code{"fcrdns"}, @code{"rdns"}, and @code{"src"} data are available > in > +all phases, but other data must have been already submitted before > they > +are available. Options with a @code{
} next to them require > the > +@code{}'s fieldname @code{data} to be an > +@code{}. There are the available options: > + > +@multitable {aaaaaaaaaaaaaaaaaaaaaaaaa} > {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} > +@item @code{"fcrdns"} > +@tab forward-confirmed reverse DNS is valid > +@item @code{"rdns"} > +@tab session has a reverse DNS > +@item @code{"rdns"}
> +@tab session has a reverse DNS in table > +@item @code{"src"}
> +@tab source address is in table > +@item @code{"helo"}
> +@tab helo name is in table > +@item @code{"auth"} > +@tab session is authenticated > +@item @code{"auth"}
> +@tab session username is in table > +@item @code{"mail-from"}
> +@tab sender address is in table > +@item @code{"rcpt-to"}
> +@tab recipient address is in table > +@end multitable > + > +These conditions may all be negated by setting > +@code{(opensmtpd-option (bool #f))}. > + > +Any conditions that require a table may indicate that tables include > regexs > +setting @code{(opensmtpd-option (regex #t))}. > + > +@item @code{decision} > +A string decision to be taken. Some decisions require an > @code{message} > +or @code{value}.=C2=A0 The value and message may be put in the > +@code{}'s fieldname @code{data}.=C2=A0 Valid strings > are: > + > +@multitable {aaaaaaaaaaaaaaaaaaaaaa} > {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} > +@item @code{"bypass"} > +@tab the session or transaction bypasses filters > +@item @code{"disconnect"} message > +@tab the session is disconnected with message > +@item @code{"junk"} > +@tab the session or transaction is junked, > +@item > +@tab=C2=A0 i.e., an =E2=80=98X-Spam: yes=E2=80=99 header is added to > +@item > +@tab any messages > +@item @code{"reject"} message > +@tab the command is rejected with message > +@item @code{"rewrite"} value > +@tab the command parameter is rewritten with value > +@end multitable > + > +Decisions that involve a message require that the message be RFC > valid, > +meaning that they should either start with a 4xx or 5xx status code. > +Descisions can be taken at any phase, though junking can only happen > before > +a message is committed. > + > +@item @code{message} (default @code{#f}) > +A string message beginning with a 4xx or 5xx status code. > + > +@item @code{value} (default: @code{#f}) > +A number value.=C2=A0 @code{value} and @code{message} are mutually > exclusive. > +@end itemize > + > +@item Data Type: opensmtpd-option > +This data type represents the configuration of an > +@code{}, which is used by > +@code{} and @code{} > +to match various options for email. > + > +@itemize > +@item @code{option} (default @code{#f}) string > + > +A string option to be taken. Some options require the fieldname > +@code{data} to have a string or an @code{}. When > the > +option record is used inside of an @code{}, > then > +valid strings for fieldname @code{option} are: > + > +@itemize > +@item @code {"fcrdns"} > +@item @code {"rdns"} > +@item @code {"src"} > +@item @code {"helo"} > +@item @code {"auth"} > +@item @code {"mail-from"} > +@item @code {"rcpt-to"} > +@end itemize > + > +When @code{} is used inside of an > +@code{}, then valid strings for fieldname > @code{option} > +are: > + > +@itemize > +@item @code {"for"} > +@item @code {"for any"} > +@item @code {"for local"} > +@item @code {"for domain"} > +@item @code {"for rcpt-to"} > +@item @code {"from any"} > +@item @code {"from auth"} > +@item @code {"from local"} > +@item @code {"from mail-from"} > +@item @code {"from rdns"} > +@item @code {"from socket"} > +@item @code {"from src"} > +@item @code {"auth"} > +@item @code {"helo"} > +@item @code {"mail-from"} > +@item @code {"rcpt-to"} > +@item @code {"tag"} > +@item @code {"tls"} > +@end itemize > + > +@item @code{data} (default @code{#f}) string | @code{ table>} > +Some options require a string or @code{} to be > +present. One would specify that table here. > + > +@item @code{regex} (default: @code{#f}) boolean > +Any options using a table may indicate that tables hold regular > +expressions by setting this option to @code{#t}. > + > +@item @code{bool} (default: @code{#t}) boolean > +When @code{(bool #f)}, this option record is negated. > +@end itemize > + > +@item Data Type: opensmtpd-table > +This data type represents the configuration of an > +@code{}. > + > +@itemize > +@item @code{name} (default @code{#f}) > +@code{name} is the name of the @code{} record. > + > +@item @code{data} (default: @code{#f}) string | list | alist | > nested-list > +@code{data} expects a string, a list of strings, an alist of > strings, or > +a nested list of strings. > +eg: > + > +@itemize > + > +@item string > +@lisp > +(data "dev@@gnu.org") > +@end lisp > + > +A table of this type is called a @code{string table}. > + > +@item list > +@lisp > +(data (list ("gnu.org" "fsf.org"))) > +@end lisp > + > +A table of this type is called a @code{list table}. > + > +@item alist > +@lisp > +(data '(("james" . "$encryptedPassword") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ("jennifer" . "$encryptedPass= word2))) > +@end lisp > + > +A table of this type is called a @code{mapping table}. > + > +@item nested-list > +@lisp > +(data '(("user1" "root@@gnu.org" "admin@@gnu.org") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ("user2" "james@@guix.gnu.org= " "sarah@@fsf.org"))) > +@end lisp > + > +A table of this type is also called a @code{mapping table}. > + > +@end itemize > +@end itemize > + > +@item Data Type: opensmtpd-pki > +This data type represents the configuration of an > +@code{}. > + > +@itemize > +@item @code{domain} (default @code{#f}) > +@code{domain} is the string name of the @code{} > record. > + > +@item @code{cert} (default: @code{#f}) > +@code{cert} (default: @code{#f}) > + > +@code{cert} is the string certificate filename to use for this pki. > + > +@item @code{key} (default: @code{#f}) > +@code{key} is the string certificate falename to use for this pki. > + > +@item @code{dhe} (default: @code{"none"}) > +Specify the DHE string parameter to use for DHE cipher suites with > host > +pkiname. Valid parameter values are @code{"none"}, @code{"legacy"}, > or > +@code{"auto"}. For @code{"legacy"}, a fixed key length of 1024 bits > is > +used, whereas for @code{"auto"}, the key length is determined > +automatically. The default is @code{"none"}, which disables DHE > cipher > +suites. > +@end itemize > + > +@item Data Type: opensmtpd-maildir > +@itemize > +@item @code{pathname} (default: @code{"~/Maildir"}) > +Deliver the message to the maildir if pathname if specified, or by > default > +to @code{"~/Maildir"}. > + > +The pathname may contain format specifiers that are expanded before > use > +(see FORMAT SPECIFIERS). > + > +@item @code{junk} (default: @code{#f}) > +If the junk argument is @code{#t}, then the message will be moved to > the @samp{=E2=80=98Junk=E2=80=99} > +folder if it contains a positive @samp{=E2=80=98X-Spam=E2=80=99} header.= This folder > will be > +created under pathname if it does not yet exist. > +@end itemize > + > +@item Data Type: opensmtpd-mda > +This record lets you delegate the delivery to a command that > receives > +the message on its standard input. > + > +@itemize > +@item @code{name} > +The string name for this MDA command. > + > +@item @code{command} > +The command to that delivers the mail. > + > +The command may contain format specifiers that are expanded before > use (see > +FORMAT SPECIFIERS). > +@end itemize > + > +@item Data Type: opensmtpd-queue > +@itemize > +@item @code{compression} (default @code{#f}) > +Store queue files in a compressed format. This may be useful to save > disk > +space. > + > +@item @code{encryption} (default @code{#f}) > +Encrypt queue files with EVP@math{_aes}@math{_256}@math{_gcm}(3). If > no key is specified, it is > +read with getpass(3). If the string stdin or a single dash (=E2=80=98-= =E2=80=99) is > given > +instead of a key, the key is read from the standard input. > + > +@item @code{ttl-delay} (default @code{#f}) > +Set the default expiration time for temporarily undeliverable > messages, > +given as a positive decimal integer followed by a unit s, m, h, or > d. The > +default is four days (@code{"4d"}). > +@end itemize > + > +@item Data Type: opensmtpd-smtp > +Data type representing an @code{} record. > + > +@itemize > +@item @code{ciphers} (default: @code{#f}) > +Set the control string for > SSL@math{_CTX}@math{_set}@math{_cipher}@math{_list}(3).=C2=A0 The default > is > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ``HIGH:!aNULL:!MD5''. > + > +@item @code{limit-max-mails} (default: @code{100}) > +Limit the number of messages to count for each sessio > + > +@item @code{limit-max-rcpt} (default: @code{1000}) > +Limit the number of recipients to count for each transaction. > + > +@item @code{max-message-size} (default: @code{35M}) > +Reject messages larger than size, given as a positive number of > bytes or as > +a string to be parsed with scan@math{_scaled}(3). > + > +@item @code{sub-addr-delim character} (default: @code{+}) > +When resolving the local part of a local email address, ignore the > ASCII > +character and all characters following it. This is helpful for email > +filters. @samp{"admin+bills@@gnu.org"} is the same email address as > +@samp{"admin@@gnu.org"}. BUT an email filter can filter emails > addressed to first > +email address into a 'Bills' email folder. > +@end itemize > + > +@item Data Type: opensmtpd-srs > +Use this record to set up the Sender Rewriting Scheme (SRS). > + > +@itemize > +@item @code{key} (default: @code{#f}) > +Set the secret key to use for SRS, the Sender Rewriting Scheme. > + > +@item @code{backup-key} (default: @code{#f}) > +Set a backup secret key to use as a fallback for SRS@. This can be > used to > +implement SRS key rotation. > + > +@item @code{ttl-delay} (default: @code{"4d"}) > +Set the time-to-live delay for SRS envelopes. After this delay, a > bounce > +reply to the SRS address will be discarded to limit risks of forged > +addresses. > +@end itemize > + > +@item Format Specifiers > +Some configuration records support expansion of their parameters at > +runtime. Such records (for example > +@code{}, @code{}) may use > +format specifiers which are expanded before delivery or relaying. > The > +following formats are currently supported: > + > +@multitable {aaaaaaaaaaaaaaaaaaa} > {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} > +@item @samp{%@{sender@}} > +@tab sender email address, may be empty string > +@item @samp{%@{sender.user@}} > +@tab user part of the sender email address, may be empty > +@item @samp{%@{sender.domain@}} > +@tab domain part of the sender email address, may be empty > +@item @samp{%@{rcpt@}} > +@tab recipient email address > +@item @samp{%@{rcpt.user@}} > +@tab user part of the recipient email address > +@item @samp{%@{rcpt.domain@}} > +@tab domain part of the recipient email address > +@item @samp{%@{dest@}} > +@tab recipient email address after expansion > +@item @samp{%@{dest.user@}} > +@tab user part after expansion > +@item @samp{%@{dest.domain@}} > +@tab domain part after expansion > +@item @samp{%@{user.username@}} > +@tab local user > +@item @samp{%@{user.directory@}} > +@tab home directory of the local user > +@item @samp{%@{mbox.from@}} > +@tab name used in mbox From separator lines > +@item @samp{%@{mda@}} > +@tab mda command, only available for mda wrappers > +@end multitable > + > +Expansion formats also support partial expansion using the optional > bracket notations > +with substring offset.=C2=A0 For example, with recipient domain > @samp{=E2=80=9Cexample.org=E2=80=9D}: > + > +@multitable {aaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaa} > +@item @samp{%@{rcpt.domain[0]@}} > +@tab expands to =E2=80=9Ce=E2=80=9D > +@item @samp{%@{rcpt.domain[1]@}} > +@tab expands to =E2=80=9Cx=E2=80=9D > +@item @samp{%@{rcpt.domain[8:]@}} > +@tab expands to =E2=80=9Corg=E2=80=9D > +@item @samp{%@{rcpt.domain[-3:]@}} > +@tab expands to =E2=80=9Corg=E2=80=9D > +@item @samp{%@{rcpt.domain[0:6]@}} > +@tab expands to =E2=80=9Cexample=E2=80=9D > +@item @samp{%@{rcpt.domain[0:-4]@}} > +@tab expands to =E2=80=9Cexample=E2=80=9D > +@end multitable > + > +In addition, modifiers may be applied to the token.=C2=A0 For example, > with recipient > +@samp{=E2=80=9CUser+Tag@@Example.org=E2=80=9D}: > + > +@multitable {aaaaaaaaaaaaaaaaaaaaaaaa} > {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} > +@item @samp{%@{rcpt:lowercase@}} > +@tab expands to =E2=80=9Cuser+tag@@example.org=E2=80=9D > +@item @samp{%@{rcpt:uppercase@}} > +@tab expands to =E2=80=9CUSER+TAG@@EXAMPLE.ORG=E2=80=9D > +@item @samp{%@{rcpt:strip@}} > +@tab expands to =E2=80=9CUser@@Example.org=E2=80=9D > +@item @samp{%@{rcpt:lowercasestrip@}} > +@tab expands to =E2=80=9Cuser@@example.org=E2=80=9D > +@end multitable > + > +For security concerns, expanded values are sanitized and potentially > dangerous > +characters are replaced with =E2=80=98:=E2=80=99. In situations where th= ey are > desirable, the > +=E2=80=9Craw=E2=80=9D modifier may be applied. For example, with recipie= nt > +@samp{=E2=80=9Cuser+t?g@@example.org=E2=80=9D}: > + > +@multitable {aaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} > +@item @samp{%@{rcpt@}} > +@tab expands to =E2=80=9Cuser+t:g@@example.org=E2=80=9D > +@item @samp{%@{rcpt:raw@}} > +@tab expands to =E2=80=9Cuser+t?g@@example.org=E2=80=9D > +@end multitable > +@end itemize > + > =C2=A0@subsubheading Exim Service > =C2=A0 > =C2=A0@cindex mail transfer agent (MTA) > diff --git a/gnu/services/mail.scm b/gnu/services/mail.scm > index 43f144a42d..4175cab375 100644 > --- a/gnu/services/mail.scm > +++ b/gnu/services/mail.scm > @@ -35,6 +35,10 @@ (define-module (gnu services mail) > =C2=A0=C2=A0 #:use-module (gnu packages admin) > =C2=A0=C2=A0 #:use-module (gnu packages dav) > =C2=A0=C2=A0 #:use-module (gnu packages tls) > +=C2=A0 #:use-module (guix i18n) > +=C2=A0 #:use-module (guix diagnostics) > +=C2=A0 #:use-module (guix ui) > +=C2=A0 #:use-module (guix utils) > =C2=A0=C2=A0 #:use-module (guix records) > =C2=A0=C2=A0 #:use-module (guix packages) > =C2=A0=C2=A0 #:use-module (guix gexp) > @@ -58,10 +62,149 @@ (define-module (gnu services mail) > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 = mailbox-configuration > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 = namespace-configuration > =C2=A0 > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-table > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-table? > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-table-name > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-table-data > + > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-ca > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-ca? > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-ca-name > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-ca-file > + > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-pki > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-pki? > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-pki-domain > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-pki-cert > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-pki-key > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-pki-dhe > + > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-local-delivery > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-local-delivery? > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-local-delivery-method > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-local-delivery-alias > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-local-delivery-ttl > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-local-delivery-user > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-local-delivery-userbase > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-local-delivery-virtual > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-local-delivery-wrapper > + > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-maildir > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-maildir? > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-maildir-pathname > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-maildir-junk > + > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-mda > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-mda-name > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-mda-command > + > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-lmtp > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-lmtp-destination > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-lmtp-rcpt > + > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-relay > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-relay? > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-relay-name > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-relay-backup > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-relay-backup-mx > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-relay-helo > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-relay-domain > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-relay-host > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-relay-pki > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-relay-srs > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-relay-tls > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-relay-auth > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-relay-mail-from > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-relay-src > + > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-option > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-option? > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-option-option > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-option-bool > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-option-regex > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-option-data > + > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-filter-phase > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-filter-phase? > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-filter-phase-name > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-filter-phase-phase > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-filter-phase-options > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-filter-phase-decision > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-filter-phase-message > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-filter-phase-value > + > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-filter > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-filter? > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-filter-name > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-filter-proc > + > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface? > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface-interface > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface-family > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface-auth > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface-auth-optional > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface-filters > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface-hostname > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface-hostnames > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface-mask-src > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface-disable-dsn > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface-pki > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface-port > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface-proxy-v2 > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface-received-auth > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface-senders > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface-masquerade > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface-secure-connection > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-interface-tag > + > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-socket > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-socket? > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-socket-filters > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-socket-mask-src > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-socket-tag > + > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-match > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-match? > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-match-action > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-match-options > + > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-smtp > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-smtp? > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-smtp-ciphers > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-smtp-limit-max-mails > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-smtp-limit-max-rcpt > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-smtp-max-message-size > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-smtp-sub-addr-delim character > + > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-srs > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-srs? > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-srs-key > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-srs-backup-key > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-srs-ttl-delay > + > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-queue > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-queue? > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-queue-compression > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-queue-encryption > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-queue-ttl-delay > + > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 = opensmtpd-configuration > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 = opensmtpd-configuration? > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-package > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-config-file > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-configuration-bounce > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-configuration-cas > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-configuration-interfaces > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-configuration-socket > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-configuration-includes > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-configuration-matches > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ;;ope= nsmtpd-configuration-mda-wrappers > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-configuration-mta-max-deferred > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-configuration-srs > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-configuration-smtp > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 opens= mtpd-configuration-queue > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 = opensmtpd-service-type > -=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 %defa= ult-opensmtpd-config-file > =C2=A0 > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 = mail-aliases-service-type > =C2=A0 > @@ -1641,22 +1784,2351 @@ (define (generate-dovecot-documentation) > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (listeners unix-listener-confi= guration fifo-listener- > configuration > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 inet-listener-configuration)) > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (protocol-configuration ,protocol-co= nfiguration-fields)) > -=C2=A0 'dovecot-configuration)) > +=C2=A0=C2=A0 'dovecot-configuration)) > =C2=A0 > =C2=A0=0C > -;;; > =C2=A0;;; OpenSMTPD. > =C2=A0;;; > +;;; This next bit of code helps me create my own sanitizer > functions. > + > +;; some fieldnames have a default value of #f, which is ok.=C2=A0 They > cannot have > +;; a value of #t. > +;; for example opensmtpd-table-data can be #f, BUT NOT true. > +;; my/sanitize procedure tests values to see if they are of the > right kind. > +;; procedure false? is needed to allow fields like 'values' to be > blank, > +;; (empty), or #f BUT also have a value like a list of strings. Use less egocentric comments ;) > +(define (false? var) > +=C2=A0 (eq? #f var)) > + > +;; TODO I have to have this procedure, or I need to change > my/sanitize > +;; procedure. > +(define (my-file-exists? file) > +=C2=A0 (and (string? file) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (access? file F_OK))) Does file-exists? not work for you? > +;; This procedure takes in a var and a list of procedures.=C2=A0 It loop= s > through > +;; list of procedures passing in var to each. > +;; if one procedure returns #t, the function returns true.=C2=A0 > Otherwise #f. > +;; TODO for fun rewrite this using map > +;; If I rewrote it in map, then it may help with sanitizing. > +;; eg: I could then potentially easily sanitize vars with lambda > procedures. > +(define (is-value-right-type? var list-of-procedures record > fieldname) > +=C2=A0 (if (null? list-of-procedures) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 #f > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (if ((car list-of-procedures) var) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 #t > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (is-value-right-t= ype? var (cdr list-of-procedures) record > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 fieldname)))) Alternatively, (any (cut <> var) list-of-procedures). > +;; converts strings like this: > +;; "apple, ham, cherry" -> "apple, ham, or cherry" > +;; "pineapple" -> "pinneapple". > +;; "cheese, grapefruit, or jam" -> "cheese, grapefruit, or jam" > +(define (add-comma-or string) > +=C2=A0 (define last-comma-location (string-rindex string #\,)) > +=C2=A0 (if last-comma-location > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (if (string-contains string ", or" last-c= omma-location) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 string > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (string-replace s= tring ", or" last-comma-location > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0 (+ 1 last-comma-location))) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 string)) > + > + > +(define (list-of-procedures->string procedures) > +=C2=A0 (define string > +=C2=A0=C2=A0=C2=A0 (let loop ((procedures procedures)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (if (null? procedures) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "" > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (begin > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (stri= ng-append > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= (cond ((eq? false? (car procedures)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "#f, ") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? boolean? (car procedures)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "a boolean, ") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? string? (car procedures)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "a string, ") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? integer? (car procedures)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "an integer, ") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? list-of-strings? (car procedures= )) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "a list of strings, ") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? assoc-list? (car procedures)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "an association list of strings,= ") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? nested-list? (car procedures)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "a nested-list of strings, ") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? opensmtpd-pki? (car procedures)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "an record, ") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? opensmtpd-table? (car procedures= )) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "an record, ") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? list-of-opensmtpd-match? (car pr= ocedures)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "a list of unique records, ") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? list-of-strings-or-gexps? (car p= rocedures)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "a list of strings or gexps, ") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ;; TODO can I remove the next two proc= edures? > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ;; tables-data-are-a* ?=C2=A0 I think = I can. > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? tables-data-are-assoc-list? (car > procedures)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (string-append > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "an reco= rd whose fieldname > 'data' are " > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "an assoc-list.\nFor examp= le: (opensmtpd-table=C2=A0 > " > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "(name \"hostnames\") , " > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "(data '((\"124.394.23.1\"= . \"gnu.org\"))))")) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? tables-data-are-a-list-of-string= s? > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (c= ar procedures)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (string-append > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "on reco= rd whose fieldname > 'data' is " > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "a list of strings.\n" > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "For example: (opensmtpd-t= able (name > \"domains\") , " > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "(data (list \"gnu.org\" \= "guix.gnu.org\")))")) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? my-file-exists? (car procedures)= ) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "a file, ") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (else "has an incorrect value, ")) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= (loop (cdr procedures))))))) > +=C2=A0 (add-comma-or (string-append (string-drop-right string 2) ".\n"))= ) (define (procedure->string) ...) (define (procedures->string list) (define strings (map procedure->string list)) (string-append (string-join (drop-right strings 1) ", ") (if (> (length list) 1) ", or") (last strings) ".\n")) > +(define (list-of-strings-or-gexps? list) > +=C2=A0 (and (list? list) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (cond ((null? list) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0 #t) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= ((or (string? (car list)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (gexp? (car list)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (local-file? (car list)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (file-append? (car list)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (plain-file? (car list)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (computed-file? (car list)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (program-file? (car list))) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0 (list-of-strings-or-gexps? (cdr list))) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= (else #f)))) > + > +(define (my/sanitize var record fieldname list-of-procedures) > +=C2=A0 (define try-string > +=C2=A0=C2=A0=C2=A0 (string-append "Try " (list-of-procedures->string lis= t-of- > procedures))) > +=C2=A0 (if (is-value-right-type? var list-of-procedures record fieldname= ) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 var > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (begin > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (cond ((string? var) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0 (report-error (G_ "(~a \"~a\") is invalid.~%") > fieldname var)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0 ((or (number? var) (boolean? var)) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0 (report-error (G_ "(~a ~a) is invalid.~%") fieldname > var) ) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0 (else > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0 (report-error (G_ "(~a ...) is invalid.~%Value is: > ~a~%") > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0 fieldname var))) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (display-hint (G_ try-string)= ) > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (throw 'bad! var)))) This procedure needs a proper name, like sanitize/check-type, but more importantly, why not simply use define-configuration? Cheers From debbugs-submit-bounces@debbugs.gnu.org Tue Dec 27 19:16:42 2022 Received: (at 56046) by debbugs.gnu.org; 28 Dec 2022 00:16:42 +0000 Received: from localhost ([127.0.0.1]:56774 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pAK7b-0003iW-9g for submit@debbugs.gnu.org; Tue, 27 Dec 2022 19:16:42 -0500 Received: from mx1.dismail.de ([78.46.223.134]:22795) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pAK7V-0003iB-VH for 56046@debbugs.gnu.org; Tue, 27 Dec 2022 19:16:38 -0500 Received: from mx1.dismail.de (localhost [127.0.0.1]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 854a6e1a; Wed, 28 Dec 2022 01:16:26 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; d=dismail.de; h=from:to:cc :subject:in-reply-to:references:date:message-id:mime-version :content-type:content-transfer-encoding; s=20190914; bh=7wtudcRk 9JQArJUw3ZCXpsFw8rPce1SXXcWJT0t0jPw=; b=LJV8hIYCzwN3KOfuBYxmz/zD 2Ym52RLsZiJHRiSuqViLDw1/L1rVLqP0NgDn4+Q4XfojT6fmMtjri5g0htWS47of 4NnFDwrmq+V6mBeG15gcx5ITqeiT9sQRQ3bAY4A59ORivGHFwn87r7V9SS/VKGEG h88rAs4AJopHoBShlIWGGxaAPbkCG3/OogZXhiwSXj5abGWZ2vdkiujPF9By6H8v aISXyr2+HsgaGt1PHcmgX/JY81xopB7O66sRbt3n4Aq8SYy/en7rykB2Eb3EZtXA 1P++C0zL2t3TVO+VdDSdphlPvxVO8BmOiNGD27n0ZlznyU1hyxvVxlWpIa557Q== Received: from smtp2.dismail.de ( [10.240.26.12]) by mx1.dismail.de (OpenSMTPD) with ESMTP id 0509335b; Wed, 28 Dec 2022 01:16:26 +0100 (CET) Received: from smtp2.dismail.de (localhost [127.0.0.1]) by smtp2.dismail.de (OpenSMTPD) with ESMTP id fe1e29eb; Wed, 28 Dec 2022 01:16:26 +0100 (CET) Received: by dismail.de (OpenSMTPD) with ESMTPSA id 21a917ca (TLSv1.3:TLS_AES_256_GCM_SHA384:256:NO); Wed, 28 Dec 2022 01:16:23 +0100 (CET) From: Joshua Branson To: Liliana Marie Prikler Subject: Re: [PATCH opensmtpd-records v3] services (opensmtpd): add opensmtpd records to enhance opensmtpd-configuration. In-Reply-To: <37239abae388f1fadcba559b62c0aae07de9d319.camel@gmail.com> (Liliana Marie Prikler's message of "Mon, 26 Dec 2022 20:34:43 +0100") References: <8001f404c0e54baba71ef9c5536abc54275c4167.1671803496.git.joshua@gnucode.me> <37239abae388f1fadcba559b62c0aae07de9d319.camel@gmail.com> X-Gnus-Sucks: I know man Date: Tue, 27 Dec 2022 19:16:15 -0500 Message-ID: <87k02co01c.fsf@dismail.de> MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable X-Spam-Score: -0.7 (/) X-Debbugs-Envelope-To: 56046 Cc: Joshua Branson , 56046@debbugs.gnu.org X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.7 (-) Liliana Marie Prikler writes: > Am Freitag, dem 23.12.2022 um 08:52 -0500 schrieb Joshua Branson: >>=20 >> * gnu/services/mail.scm: >> (opensmtpd-table, >> opensmtpd-ca, >> opensmtpd-pki, >> opensmtpd-action-local-delivery, >> opensmtpd-maildir, >> opensmtpd-mda, >> opensmtpd-lmtp, >> opensmtpd-relay, >> opensmtpd-option, >> opensmtpd-filter-phase, >> opensmtpd-filter, >> opensmtpd-interface, >> opensmtpd-socket, >> opensmtpd-match, >> opensmtpd-smtp, >> opensmtpd-srs, >> opensmtpd-queue, and >> opensmtpd-configuration): New records. > Don't forget to put closing parentheses at the end of lines. Also, > feel free to group them to save vertical space. Thanks again for your speedy review! I am sending this from my gnucode.me e= mail address you'll notice. And it is using these new records. :) > >> (false?, is-value-right-type, add-comma-or-string, >> list-of-procedures->string, string-in-list?, my-sanitize, >> opensmtpd-filter-chain?, throw-error-duplicate-option, >> sanitize-list-of-options-for-match, sanitize-filters, >> list-has-duplicates-or-non-filters?, >> filter-phase-has-message-and-value?, >> filter-phase-decision-lacks-proper-message?, >> filter-phase-lacks-proper-value?, >> filter-phase-has-incorrect-junk-or-bypass?, >> filter-phase-junks-after-commit?, >> list-of-unique-filter-or-filter-phase?, throw-error, >> contains-duplicate?, list-of-type?, list-of-strings?, >> list-of-unique-opensmtpd-option?, >> list-of-opensmtpd-ca?, >> list-of-opensmtpd-pki?, >> list-of-opensmtpd-listen-on?, >> list-of-unique-opensmtpd-match?, list-of-strings->string, >> assoc-list? assoc-list, variable->string, >> tables-data-are-assoc-list?, >> tables-data-are-a-list-of-strings?, >> table-data-are-a-nested-list-of-strings?, >> assoc-list->string, >> opensmtpd-table->string, >> opensmtpd-listen-on->string, >> opensmtpd-listen-on-socket->string, >> opensmtpd-action-relay->string, >> opensmtpd-lmtp->string, >> opensmtpd-mda->string, >> opensmtpd-maildir->string, >> opensmtpd-action-local-delivery->string, >> opensmtpd-action->string, opensmtpd-option->string, >> opensmtpd-match->string, >> opensmtpd-ca->string, opensmtpd-pki->string, >> generate-filter-chain-name, opensmtpd-filter-chain->string, >> opensmtpd-filter-phase->string, opensmtpd-filters->string, >> opensmtpd-listen->string, >> opensmtpd-srs->string, >> opensmtpd-smtp->string, >> opensmtpd-queue->string, get-opensmtpd-actions, >> get-opensmtpd-pkis, get-opensmtpd-filters, flatten, >> get-opensmtpd-tables, opensmtpd-fieldname->string, >> list-of-records->string, opensmtpd->mixed-text-file): New >> procedures. >>=20 >> * gnu/tests/mail.scm : new tests for various opensmtpd records. >>=20 >> * doc/guix.texi (OpenSMTPD Service): Added documentation for the >> new records for opensmtpd. >> --- >> =C2=A0doc/guix.texi=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 | 10= 65 ++++++++++++++++- >> =C2=A0gnu/services/mail.scm | 2560 >> ++++++++++++++++++++++++++++++++++++++++- >> =C2=A0gnu/tests/mail.scm=C2=A0=C2=A0=C2=A0 |=C2=A0 713 ++++++++++++ >> =C2=A03 files changed, 4310 insertions(+), 28 deletions(-) >>=20 >> diff --git a/doc/guix.texi b/doc/guix.texi >> index 535c8cdfc3..879a2ad233 100644 >> --- a/doc/guix.texi >> +++ b/doc/guix.texi >> @@ -25407,16 +25407,66 @@ could instantiate a dovecot service like >> this: >> =C2=A0@end lisp >> =C2=A0 >> =C2=A0@subsubheading OpenSMTPD Service >> +@cindex opensmtpd >> =C2=A0 >> =C2=A0@deffn {Scheme Variable} opensmtpd-service-type >> -This is the type of the @uref{https://www.opensmtpd.org, OpenSMTPD} >> -service, whose value should be an @code{opensmtpd-configuration} >> object >> -as in this example: >> +OpenSMTPD is an easy-to-use mail transfer agent (MTA).=C2=A0 OpenSMTPD >> +@strong{listens} for incoming mail and @strong{matches} the mail to >> +@strong{actions}. The following records represent those stages: >> =C2=A0 >> -@lisp >> -(service opensmtpd-service-type >> -=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-configurati= on >> -=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (config-fi= le (local-file "./my-smtpd.conf")))) >> +@multitable {aaaaaaaaa} >> {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} > I hope this will receive proper documentation. Users can still use (service opensmtpd-service-type =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-configuration =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (config-file (= local-file "./my-smtpd.conf")))) config-file is still a fieldname of . Is that wha= t you mean? Also the generated documentation is available here: https://notabug.org/jbranso/linode-guix-system-configuration/src/master/ope= nsmtpd-records-documentation.txt >> +@item @strong{listens} >> +@tab @code{} >> +@item >> +@tab @code{} >> +@item >> +@tab >> +@item @strong{matches} >> +@tab @code{} >> +@item >> +@tab >> +@item @strong{actions} >> +@tab @code{} >> +@item >> +@tab @code{} >> +@end multitable >> + >> +Additionally, each @code{} and >> +@code{} may use a list of @code{> filter>}, >> +and/or @code{} records to filter >> +email/spam. Also numerous records' fieldnames use >> +@code{} records to hold lists or key value pairs of >> +data.=C2=A0 Be sure to read the @code{} section to lea= rn >> the >> +differance between a @code{mapping table} and a @code{list table}. >> + >> +Finally, both @code{} and >> +@code{} use @code{} to >> +configure various options. >> + >> +A simple example opensmtpd configuration is below: >> + >> +@lisp >> +(let ((smtp.gnu.org (opensmtpd-pki >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (doma= in "smtp.gnu.org") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (cert= "file.cert") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (key = "file.key")))) >> +=C2=A0 (service opensmtpd-service-type >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd= -configuration >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (int= erfaces (list >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= (opensmtpd-interface >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0 (pki smtp.gnu.org)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= (opensmtpd-interface >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0 (pki smtp.gnu.org) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0 (secure-connection "smtps")))) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (mat= ches (list >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-match >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (action >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (open= smtpd-local-delivery >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= (name "local-delivery")))) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-match >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (action >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (open= smtpd-relay >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= (name "relay"))))))))) >> =C2=A0@end lisp >> =C2=A0@end deffn >> =C2=A0 >> @@ -25425,7 +25475,7 @@ Data type representing the configuration of >> opensmtpd. >> =C2=A0 >> =C2=A0@table @asis >> =C2=A0@item @code{package} (default: @var{opensmtpd}) >> -Package object of the OpenSMTPD SMTP server. >> +Package object of the OpenSMTPD server. >> =C2=A0 >> =C2=A0@item @code{config-file} (default: @code{%default-opensmtpd-config- >> file}) >> =C2=A0File-like object of the OpenSMTPD configuration file to use.=C2=A0= By >> default >> @@ -25433,14 +25483,1013 @@ it listens on the loopback network >> interface, and allows for mail from >> =C2=A0users and daemons on the local machine, as well as permitting email >> to >> =C2=A0remote servers.=C2=A0 Run @command{man smtpd.conf} for more inform= ation. >> =C2=A0 >> +@item @code{bounce} (default: @code{(list "4h")}) >> +@code{bounce} is a list of strings, which send warning messages to >> the >> +envelope sender when temporary delivery failures cause a message to >> +remain in the queue for longer than a specified delay. Each delay >> option >> +is a string parameter beginning with a positive decimal integer and >> a >> +unit, which can be 's', 'm', 'h', or 'd'. At most four delay >> parameters >> +can be specified. >> + >> +@item @code{interfaces} default: >> +@lisp >> +(list >> +=C2=A0 (opensmtpd-interface >> +=C2=A0=C2=A0=C2=A0 (interface "lo") >> +=C2=A0=C2=A0=C2=A0 (port 25))) >> +@end lisp >> +@code{interfaces} is a list of @code{} records. >> +This list details what interfaces and ports OpenSMTPD listens on as >> well as >> +other options. >> + >> +@item @code{socket} (default: @code{(opensmtpd-socket)}) >> +Listens for incoming connections on the Unix domain socket. >> + >> +@item @code{includes} (default: @code{#f}) >> +@code{includes} is a list of string filenames. Each filename's >> contents is >> +additional configuration that is inserted into the top of the >> configuration >> +file.=C2=A0 Run @code{man smtpd.conf} for more information. >> + >> +@item @code{matches} default: >> +@lisp >> +(list (opensmtpd-match >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (action (opensmtpd-local-delivery >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0 (name "local") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0 (method "mbox") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0 (options >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0 (list >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-option >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (option "for local"))))))) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-match >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (action (opensmtpd-relay >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0 (name "outbound"))) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (options >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (list >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-option >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (option "from lo= cal")) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-option >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (option "for any= ")))))) >> +@end lisp >> +@code{matches} is a list of @code{} records, which >> +matches incoming mail and sends it to a correspending action. The >> match >> +records are evaluated sequentially, with the first match winning. >> +Therefore @emph{the order that you arrange your matches is >> important}. >> +It's a good idea to put specific matches first and an all >> emcompassing >> +match (like @code{(option "for any")}) @strong{last}. If an incoming >> +mail does not match any match records, then it is rejected. >> + >> +@item @code{mta-max-deferred} (default: @code{100}) >> +When delivery to a given host is suspended due to temporary >> failures, cache >> +at most number envelopes for that host such that they can be >> delivered as >> +soon as another delivery succeeds to that host. The default is 100. >> + >> +@item @code{queue} (default: @code{#f}) >> +@code{queue} expects an @code{} record. With it, >> one may >> +compress and encrypt queue-ed emails as well as set the default >> expiration >> +time for temporarily undeliverable messages. >> + >> +@item @code{smtp} (default: @code{#f}) >> +@code{smtp} expects an @code{} record, which lets >> one >> +specifiy how large email may be along with other settings. >> + >> +@item @code{srs} (default: @code{#f}) >> +@code{srs} expects an @code{} record, which lets one >> set >> +up SRS, the Sender Rewritting Scheme. >> + >> =C2=A0@item @code{setgid-commands?} (default: @code{#t}) >> =C2=A0Make the following commands setgid to @code{smtpq} so they can be >> =C2=A0executed: @command{smtpctl}, @command{sendmail}, @command{send- >> mail}, >> =C2=A0@command{makemap}, @command{mailq}, and @command{newaliases}. >> =C2=A0@xref{Setuid Programs}, for more information on setgid programs. >> + >> =C2=A0@end table >> =C2=A0@end deftp >> =C2=A0 >> +@itemize >> +@item Data Type: opensmtpd-interface >> +Data type representing the configuration of an >> +@code{}. It listens on the fieldname >> +@code{interface} for incoming connections, using the same syntax as >> +@code{ifconfig}. The interface parameter may also be an string >> interface >> +group, an string IP address, or a string domain name. Listening can >> +optionally be restricted to a specific address via the fieldname >> +@code{family}, which can be either @code{"inet4"} or @code{"inet6"}. >> + >> +@itemize >> +@item @code{interface} (default: @code{"lo"}) >> + >> +The string interface to listen for incoming connections.=C2=A0 This >> string >> +may be an interface group, an IP address, or a domain name. These >> +interfaces can usually be found by the command @code{ip link}. >> + >> +@item @code{family} (default: @code{#f}) >> + >> +Only listen on a specific address family.=C2=A0 Valid strings are >> +@code{"inet4"} or @code{"inet6"}, which will only listen on IPv4 or >> IPv6 >> +respectfully.=C2=A0 If @code{(family #f)}, then opensmtpd will listen on >> both >> +IPv4 and IPv6. >> + >> +@item @code{auth} (default: @code{#f}) >> +Support SMTPAUTH: clients may only start SMTP transactions after >> +successful authentication. If @code{auth} is @code{#t}, then users >> are >> +authenticated against their own normal login credentials. >> Alternatively >> +@code{auth} may be a @code{mapping table} that maps usernames to >> +encrypted passwords.=C2=A0 The password can be encrypted via the >> +@code{smtpctl} @code{encrypt} subcommand. >> + >> +@item @code{auth-optional} (default: @code{#f}) >> +Support SMTPAUTH optionally: clients need not authenticate, but may >> do >> +so.=C2=A0 This allows the @code{} to both accept >> +incoming mail from untrusted senders and permit outgoing mail from >> +authenticated users. It can be used in situations where it is not >> +possible to listen on a separate port (usually the submission port, >> 587) >> +for users to authenticate.=C2=A0 This option also accepts a @code{mappi= ng >> +table} that maps usernames to encrypted passwords. >> + >> +@item @code{filters} (default: @code{#f}) >> +A list of one or many @code{} or >> +@code{} records. The filters are applied >> +sequentially. These records listen and filter on connections handled >> by this >> +listener. >> + >> +@item @code{hostname} (default: @code{#f}) >> +Change the default server name in the greeting banner instead of the >> +default one. >> + >> +@item @code{hostnames} (default: @code{#f}) >> +Override the server name for specific addresses. Use a @code{mapping >> +table} that maps string IP addresses to string hostnames. If the >> address >> +on which the connection arrives appears in the mapping, the >> associated >> +hostname is used. >> + >> +@item @code{mask-src} (default: @code{#f}) >> +If @code{#t}, then omit the from part when prepending =E2=80=9CReceived= =E2=80=9D >> headers. >> + >> +@item @code{disable-dsn} (default: @code{#f}) >> +When @code{#t}, then disable the DSN (Delivery Status Notification) >> extension. >> + >> +@item @code{pki} (default: @code{#f}) >> +For secure connections, use an @code{} record to >> prove a >> +mail server's identity. >> + >> +@item @code{port} (default: @code{25}) >> +Listen on the integer port instead of the default port of 25. >> + >> +@item @code{proxy-v2} (default: @code{#f}) >> +If @code{#t}, then support the PROXYv2 protocol, rewriting >> appropriately source >> +address received from proxy. >> + >> +@item @code{received-auth} (default: @code{#f}) >> +If @code{#t}, then in =E2=80=9CReceived=E2=80=9D headers, report whethe= r the session >> was >> +authenticated and by which local user. >> + >> +@item @code{senders} (default: @code{#f}) >> +Look up the authenticated user in the supplied @code{mapping table} >> to >> +find the email addresses that user is allowed to submit mail as. >> + >> +@item @code{masquerade} (default: @code{#f}) >> +@code{masquerade}, is used in conjunction with @code{senders}.=C2=A0 If >> +@code{#t}, then the From header is rewritten to match the sender >> +provided in the SMTP session.=C2=A0 If @code{senders} is @code{#false}, >> then >> +@code{masquerade} cannot be @code{#t}. >> + >> +@item @code{secure-connection} (default: @code{#f}) >> +This is a string of one of these options: >> + >> +@multitable {aaaaaaaaaaaaaaaaaaaa} >> {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} > Same here Are you saying secure-connection is not documented? It's documented in the table just below: >> +@item @code{"smtps"} >> +@tab Support SMTPS, by default on port 465. >> +@item @code{"tls"} >> +@tab Support STARTTLS, by default on port 25. >> +@item @code{"tls-require"} >> +@tab Like @code{"tls"}, but force clients to >> +@item >> +@tab establish a secure connection before being >> +@item >> +@tab allowed to start an SMTP transaction. >> +@item @code{"tls-require-verify"} >> +@tab Like @code{"tls-require"}, but clients must >> +@item >> +@tab also provide a valid certificate >> +@item >> +@tab to establish an SMTP session. >> +@end multitable >> + >> +@item @code{tag} (default: @code{#f}) >> +Clients connecting to the listener are tagged with the given string >> tag. >> +@end itemize >> + >> +@item Data Type: opensmtpd-socket >> +Data type representing the configuration of an >> +@code{}. Listen for incoming SMTP connections on >> the >> +Unix domain socket @samp{/var/run/smtpd.sock}. This is done by >> default, >> +even if the record is absent. >> + >> +@itemize >> +@item @code{filters} (default: @code{#f}) >> +A list of one or many @code{} or >> +@code{} records. These filter incoming >> +connections handled by this listener. >> + >> +@item @code{mask-src} (default: @code{#f}) >> +If @code{#t}, then omit the from part when prepending =E2=80=9CReceived= =E2=80=9D >> headers. >> + >> +@item @code{tag} (default: @code{#f}) >> +Clients connecting to the listener are tagged with the given string >> tag. >> +@end itemize >> + >> +@item Data Type: opensmtpd-match >> +@cindex opensmtpd-match >> +This data type represents the configuration of an >> +@code{} record. >> + >> +If at least one mail envelope matches the options of one match >> record, >> +receive the incoming message, put a copy into each matching >> envelope, >> +and atomically save the envelopes to the mail spool for later >> processing >> +by the respective @code{} found in fieldname >> +@code{action}.=C2=A0 Here is an example @code{opensmtpd-match} >> +record. >> + >> +@lisp >> +(opensmtpd-match >> + (action (opensmtpd-local-delivery >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (name "receive") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (method (opensmt= pd-maildir >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (pathname "/home/%@{rcpt.user@}/Mai= ldir") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (junk #t))) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (virtual (opensm= tpd-table >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (name "virt") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (data '(("carmen" . "carmen@@= gnu.org"))))))) >> + (options (list (opensmtpd-option >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0 (option "from any")) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0 (opensmtpd-option >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0 (option "for domain") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0 (data (opensmtpd-table >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (name= "domain-table") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (data= (list "gnu.org" "fsf.org")))))))) >> +@end lisp >> + >> +@itemize >> +@item @code{action} (default: @code{#f}) >> + >> +If mail matches this match configuration, then do this action. Valid >> values >> +include @code{} or >> +@code{}. >> + >> +@item @code{options} (default: @code{#f}) >> +The fieldname @code{option} is a list of unique >> +@code{} records. >> + >> +There are some mutually exclusive options: there can be only one >> ``for'' >> +and only one ``from'' option. >> + >> +@multitable {aaaaaaaaaaaaaaaaaaaaaaaaa} >> {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} > and here. If you are referring to is it documented like this: =E2=80=A2 Data Type: opensmtpd-option This data type represents the configuration of an =E2=80=98=E2=80=99, which is used by =E2=80=98=E2=80=99 and =E2=80=98= =E2=80=99 to match various options for email. (sorry about the above formatting). That is currently how the documentation= is generated. I need to go through the documentation is fix that. I did correctly. >> +@headitem for >> +@tab from >> +@item only use one of the following: >> +@tab only use one of the following: >> +@item @code{"for any"} >> +@tab @code{"from any"} >> +@item @code{"for local"} >> +@tab @code{"from auth"} >> +@item @code{"for domain"} >> +@tab @code{"from local"} >> +@item @code{"for rcpt-to"} >> +@tab @code{"from mail-from"} >> +@item >> +@tab @code{"from socket"} >> +@item >> +@tab @code{"from src"} >> +@end multitable >> + >> +Additionally, some options require additional data via >> +@code{}'s fieldname @code{data}.=C2=A0 The following >> list >> +will explain the below syntax. >> + >> +@itemize >> +@item @code{"for any"} >> +This option only requires fieldname @code{option} to have the string >> +@code{"for any"}: >> + >> +@lisp >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-option >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (option "for any")) >> +@end lisp >> + >> +@item @code{"tag"} _tag_ >> +This option only requires fieldname @code{option} to have the string >> +@code{"tag"} with a string in fieldname @code{data}: >> + >> +@lisp >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-option >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (option "tag") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (data "this-tag")) >> +@end lisp >> + >> +@item @code{"for rcpt"} _domain_ | >> +This option requires fieldname @code{data} to have a string domain >> or >> +@code{list table}: >> + >> +@lisp >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-option >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (option "for rcpt") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (data "gnu.org")) >> +@end lisp >> + >> +OR >> + >> +@lisp >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (opensmtpd-option >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (option "for rcpt") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (data (list "gnu.org" "fsf.org"))) >> +@end lisp >> +@end itemize >> + >> +The following matching options are supported and can all be negated >> (via not >> +#t). The options that support a table (anything surrounded with '<' >> and '>' >> +eg:
), also support specifying regex via (regex #t). >> + >> +@itemize >> +@item @code{"for any"} >> +Specify that session may address any destination. >> + >> +@item @code{"for local"} >> +Specify that session may address any local domain.=C2=A0 This is the >> default, >> +and may be omitted. >> + >> +@item @code{"for domain"} _domain_ | >> +Specify that session may address the string _domain_ or >> +@code{list table} . >> + >> +@item @code{"for rcpt-to"} _recipient_ | >> +Specify that session may address the string _recipient_ or list >> table >> +. >> + >> +@item @code{"from any"} >> +Specify that session may originate from any source. >> + >> +@item @code{"from auth"} >> +Specify that session may originate from any authenticated user, no >> matter >> +the source IP address. >> + >> +@item @code{"from auth"} _user_ | >> +Specify that the session may originate from authenticated _user_ or >> +@code{list table} , no matter the source IP address. >> + >> +@item @code{"from local"} >> +Specify that session may only originate from a local IP address, or >> from >> +the local enqueuer.=C2=A0 This is the default, and may be omitted. >> + >> +@item @code{"from mail-from"} _sender_ | >> +Specify that session may originate from _sender_ or @code{list >> table} >> +, no matter the source IP address. >> + >> +@item @code{"from rdns"} >> +Specify that session may only originate from an IP address that >> resolves >> +to a reverse DNS@. >> + >> +@item @samp{"from rdns"} _hostname_ | >> +Specify that session may only originate from an IP address that >> resolves >> +to a reverse DNS matching string _hostname_ or @code{list table} >> +. >> + >> +@item @samp{"from socket"} >> +Specify that session may only originate from the local enqueuer. >> + >> +@item @code{"from src"} _address_ |
>> +Specify that session may only originate from string _address_ or >> +@code{list table}
which can be a specific address or a >> subnet >> +expressed in CIDR-notation. >> + >> +@item @code{"auth"} >> +Matches transactions which have been authenticated. >> + >> +@item @code{"auth"} _username_ | >> +Matches transactions which have been authenticated for string _user_ >> or >> +@code{list table} . >> + >> +@item @code{"helo"} _helo-name_ | >> +Specify that session's HELO / EHLO should match the string _helo- >> name_ >> +or @code{list table} . >> + >> +@item @code{"mail-from"} _sender_ | >> +Specify that transactions's MAIL FROM should match the string >> _sender_ >> +or @code{list table} . >> + >> +@item @code{"rcpt-to"} _recipient_ | >> +Specify that transaction's RCPT TO should match the string >> _recipient_ >> +or @code{list table} . >> + >> +@item @code{"tag"} _tag_ >> +Matches transactions tagged with the given tag. >> + >> +@item @code{"tls"} >> +Specify that transaction should take place in a TLS channel. >> +@end itemize >> + >> +@end itemize >> + >> +@item Data Type: opensmtpd-local-delivery >> +This data type represents the configuration of an >> +@code{} record. >> + >> +@itemize >> +@item @code{name} (default: @code{#f}) >> +@code{name} is the string name of the relay action. >> + >> +@item @code{method} (default: @code{"mbox"}) >> +The email delivery option.=C2=A0 Valid options are: >> + >> +@itemize >> +@item @code{"mbox"} >> +Deliver the message to the user's mbox with mail.local(8). >> + >> +@item @code{"expand-only"} >> +Only accept the message if a delivery method was specified in an >> aliases >> +or .forward file. >> + >> +@item @code{"forward-only"} >> +Only accept the message if the recipient results in a remote address >> after >> +the processing of aliases or forward file. >> + >> +@item @code{} >> +Deliver the message to an LMTP server at @code{}'s >> +fieldname @code{destination}. The location may be expressed as >> string >> +host:port or as a UNIX socket. Optionally, @code{}'s >> +fieldname @code{rcpt-to} might be specified to use the recipient >> email >> +address (after expansion) instead of the local user in the LMTP >> session >> +as RCPT TO. >> + >> +@item @code{} >> +Deliver the message to the maildir in >> +@code{}'s fieldname @code{pathname} if specified, >> +or by default to @code{"~/Maildir"}. >> + >> +The pathname may contain format specifiers that are expanded before >> use >> +(see the below section about Format Specifiers). >> + >> +If @code{}'s record fieldname @code{junk} is >> @code{#t}, >> +then message will be moved to the =E2=80=98Junk=E2=80=99 folder if it c= ontains a >> positive >> +=E2=80=98X-Spam=E2=80=99 header. This folder will be created under fiel= dname >> @code{pathname} if >> +it does not yet exist. >> + >> +@item @code{} >> +Delegate the delivery to the @code{}'s fieldname >> +@code{command} (type string) that receives the message on its >> standard input. >> + >> +The @code{command} may contain format specifiers that are expanded >> before use >> +(see Format Specifiers). >> +@end itemize >> + >> +@item @code{alias} (default: @code{#f}) >> +Use the @code{mapping table} for aliases expansion. >> + >> +@item @code{ttl} (default: @code{#f}) >> +@code{ttl} is a string specify how long a message may remain in the >> queue.=C2=A0 It's >> +format is @code{n@{s|m|h|d@}}.=C2=A0 eg: @code{"4m"} is four minutes. >> + >> +@item @code{user} (default: @code{#f} ) >> +@code{user} is the string username for performing the delivery, to >> be looked up >> +with getpwnam(3). >> + >> +This is used for virtual hosting where a single username is in >> charge of >> +handling delivery for all virtual users. >> + >> +This option is not usable with the mbox delivery method. >> + >> +@item @code{userbase} (default: @code{#f}) >> +@code{userbase} is an @code{} record for mapping >> user >> +lookups instead of the getpwnam(3) function. >> + >> +The fieldnames @code{user} and @code{userbase} are mutually >> exclusive. >> + >> +@item @code{virtual} (default: @code{#f}) >> +@code{virtual} is an @code{} record is used for >> virtual >> +expansion. >> +@end itemize >> + >> +@item Data Type: opensmtpd-relay >> +This data type represents the configuration of an >> +@code{} record. >> + >> +@itemize >> +@item @code{name} (default: @code{#f}) >> +@code{name} is the string name of the relay action. >> + >> +@item @code{backup} (default: @code{#f}) >> +When @code{#t}, operate as a backup mail exchanger delivering >> messages to any >> +mail exchanger with higher priority. >> + >> +@item @code{backup-mx} (default: @code{#f}) >> +Operate as a backup mail exchanger delivering messages to any mail >> exchanger >> +with higher priority than mail exchanger identified as string name. >> + >> +@item @code{helo} (default: @code{#f}) >> +Advertise string heloname as the hostname to other mail exchangers >> during >> +the HELO phase. >> + >> +@item @code{helo-src} (default: @code{#f} ) >> + Use the mapping @code{} to look up a hostname >> +matching the source address, to advertise during the HELO phase. >> + >> +@item @code{domain} (default: @code{#f}) >> +Do not perform MX lookups but look up destination domain in an >> +@code{} and use matching relay url as relay host. >> + >> +@item @code{host} (default: @code{#f}) >> +Do not perform MX lookups but relay messages to the relay host >> described by >> +the string relay-url. The format for relay-url is >> +@samp{[proto://[label@@]]host[:port]}. The following protocols are >> available: >> + >> +@multitable {aaaaaaaaaa} >> {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} > AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA. Are you giving me a triple A+ ? :) Org generated the the like that. I think= you mentioned that I should use fractions last time. Sorry I did not do that. I= f I wait 'til I implement every one of your suggestions, I will probably never submit it. I am really probably "perfecting" this service. >> +@item @code{smtp} >> +@tab Normal SMTP session with opportunistic STARTTLS (the default). >> +@item @code{smtp+tls} >> +@tab Normal SMTP session with mandatory STARTTLS@. >> +@item @code{smtp+notls} >> +@tab Plain text SMTP session without TLS@. >> +@item @code{lmtp} >> +@tab LMTP session.=C2=A0 port is required. >> +@item @code{smtps} >> +@tab SMTP session with forced TLS on connection, default port is >> +@item >> +@tab 465. >> +@end multitable >> + >> +Unless noted, port defaults to 25. >> + >> +The label corresponds to an entry in a credentials table, as >> documented in >> +@code{man table}. It is used with the @code{"smtp+tls"} and >> @code{"smtps"} protocols for >> +authentication. Server certificates for those protocols are verified >> by >> +default. >> + >> +@item @code{pki} (default: @code{#f}) >> +For secure connections, use the certificate associated with >> +@code{} (declared in a pki directive) to prove the >> +client's identity to the remote mail server. >> + >> +@item @code{srs} (default: @code{#f}) >> +If @code{#t}, then when relaying a mail resulting from a forward, >> use the Sender >> +Rewriting Scheme to rewrite sender address. >> + >> +@item @code{tls} (default: @code{#f}) boolean or string ``no- >> verify'' > Instead of a string, take 'no-verify as symbol perhaps? Sounds good to me. May I ask why you prefer a symbol instead of a string? >> +When @code{#t}, Require TLS to be used when relaying, using >> mandatory STARTTLS by >> +default. When used with a smarthost, the protocol must not be >> +@samp{"smtp+notls://"}. When string @code{"no-verify"}, then do not >> require a valid >> +certificate. >> + >> +@item @code{auth} (default: @code{#f}) @code{} >> +Use the alist @code{} for connecting to relay-url >> +using credentials. This option is usable only with fieldname >> @code{host} option. >> + >> +@item @code{mail-from} (default: @code{#f}) string >> +Use the string mailaddress as MAIL FROM address within the SMTP >> transaction. >> + >> +@item @code{src} (default: @code{#f}) string | @code{> table>} >> +Use the string or @code{} sourceaddr for the >> +source IP address, which is useful on machines with multiple >> interfaces. If >> +the list contains more than one address, all of them are used in >> such a way >> +that traffic is routed as efficiently as possible. >> +@end itemize >> + >> +@item Data Type: opensmtpd-filter >> +This data type represents the configuration of an >> +@code{}. This is the filter record one should use >> +if they want to use an external package to filter email eg: rspamd >> or >> +spamassassin. >> + >> +@itemize >> +@item @code{name} (default: @code{#f}) >> +The string name of the filter. >> + >> +@item @code{proc} (default: @code{#f}) >> +The string command or process name.=C2=A0 If @code{proc-exec} is >> @code{#t}, @code{proc} is >> +treated as a command to execute.=C2=A0 Otherwise, it is a process name. >> + >> +@item @code{proc-exec} (default: @code{#f}) >> +If @code{#t}, then execute the command in @code{proc}. >> +@end itemize >> + >> +@item Data Type: opensmtpd-filter-phase >> +This data type represents the configuration of an >> +@code{}. >> + >> +In a regular workflow, @code{smtpd(8)} may accept or reject a >> message >> +based only on the content of envelopes. Its decisions are about the >> +handling of the message, not about the handling of an active >> session. >> + >> +Filtering extends the decision making process by allowing >> +@code{smtpd(8)} to stop at each phase of an SMTP session, check that >> +options are met, then decide if a session is allowed to move >> forward. >> + >> +With filtering via an @code{} record, a >> +session may be interrupted at any phase before an envelope is >> complete. A >> +message may also be rejected after being submitted, regardless of >> whether the >> +envelope was accepted or not. >> + >> +@itemize >> +@item @code{name} (default: @code{#f}) >> + >> +The string name of the filter phase. >> + >> +@item @code{phase-name} (default: @code{#f}) >> +The string name of the phase. Valid values are: >> + >> +@multitable {aaaaaaaaaaa} >> {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} >> +@item @code{"connect"} >> +@tab upon connection, before a banner is displayed >> +@item @code{"helo"} >> +@tab after HELO command is submitted >> +@item @code{"ehlo"} >> +@tab after EHLO command is submitted >> +@item @code{"mail-from"} >> +@tab after MAIL FROM command is submitted >> +@item @code{"rcpt-to"} >> +@tab after RCPT TO command is submitted >> +@item @code{"data"} >> +@tab after DATA command is submitted >> +@item @code{"commit"} >> +@tab after message is fully is submitted >> +@end multitable >> + >> +@item @code{options} (default @code{#f}) >> +A list of unique @code{} records. >> + >> +At each phase, various options, specified by a list of >> +@code{}, may be checked. The >> +@code{}'s fieldname @code{option} values of: >> +@code{"fcrdns"}, @code{"rdns"}, and @code{"src"} data are available >> in >> +all phases, but other data must have been already submitted before >> they >> +are available. Options with a @code{
} next to them require >> the >> +@code{}'s fieldname @code{data} to be an >> +@code{}. There are the available options: >> + >> +@multitable {aaaaaaaaaaaaaaaaaaaaaaaaa} >> {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} >> +@item @code{"fcrdns"} >> +@tab forward-confirmed reverse DNS is valid >> +@item @code{"rdns"} >> +@tab session has a reverse DNS >> +@item @code{"rdns"}
>> +@tab session has a reverse DNS in table >> +@item @code{"src"}
>> +@tab source address is in table >> +@item @code{"helo"}
>> +@tab helo name is in table >> +@item @code{"auth"} >> +@tab session is authenticated >> +@item @code{"auth"}
>> +@tab session username is in table >> +@item @code{"mail-from"}
>> +@tab sender address is in table >> +@item @code{"rcpt-to"}
>> +@tab recipient address is in table >> +@end multitable >> + >> +These conditions may all be negated by setting >> +@code{(opensmtpd-option (bool #f))}. >> + >> +Any conditions that require a table may indicate that tables include >> regexs >> +setting @code{(opensmtpd-option (regex #t))}. >> + >> +@item @code{decision} >> +A string decision to be taken. Some decisions require an >> @code{message} >> +or @code{value}.=C2=A0 The value and message may be put in the >> +@code{}'s fieldname @code{data}.=C2=A0 Valid strings >> are: >> + >> +@multitable {aaaaaaaaaaaaaaaaaaaaaa} >> {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} >> +@item @code{"bypass"} >> +@tab the session or transaction bypasses filters >> +@item @code{"disconnect"} message >> +@tab the session is disconnected with message >> +@item @code{"junk"} >> +@tab the session or transaction is junked, >> +@item >> +@tab=C2=A0 i.e., an =E2=80=98X-Spam: yes=E2=80=99 header is added to >> +@item >> +@tab any messages >> +@item @code{"reject"} message >> +@tab the command is rejected with message >> +@item @code{"rewrite"} value >> +@tab the command parameter is rewritten with value >> +@end multitable >> + >> +Decisions that involve a message require that the message be RFC >> valid, >> +meaning that they should either start with a 4xx or 5xx status code. >> +Descisions can be taken at any phase, though junking can only happen >> before >> +a message is committed. >> + >> +@item @code{message} (default @code{#f}) >> +A string message beginning with a 4xx or 5xx status code. >> + >> +@item @code{value} (default: @code{#f}) >> +A number value.=C2=A0 @code{value} and @code{message} are mutually >> exclusive. >> +@end itemize >> + >> +@item Data Type: opensmtpd-option >> +This data type represents the configuration of an >> +@code{}, which is used by >> +@code{} and @code{} >> +to match various options for email. >> + >> +@itemize >> +@item @code{option} (default @code{#f}) string >> + >> +A string option to be taken. Some options require the fieldname >> +@code{data} to have a string or an @code{}. When >> the >> +option record is used inside of an @code{}, >> then >> +valid strings for fieldname @code{option} are: >> + >> +@itemize >> +@item @code {"fcrdns"} >> +@item @code {"rdns"} >> +@item @code {"src"} >> +@item @code {"helo"} >> +@item @code {"auth"} >> +@item @code {"mail-from"} >> +@item @code {"rcpt-to"} >> +@end itemize >> + >> +When @code{} is used inside of an >> +@code{}, then valid strings for fieldname >> @code{option} >> +are: >> + >> +@itemize >> +@item @code {"for"} >> +@item @code {"for any"} >> +@item @code {"for local"} >> +@item @code {"for domain"} >> +@item @code {"for rcpt-to"} >> +@item @code {"from any"} >> +@item @code {"from auth"} >> +@item @code {"from local"} >> +@item @code {"from mail-from"} >> +@item @code {"from rdns"} >> +@item @code {"from socket"} >> +@item @code {"from src"} >> +@item @code {"auth"} >> +@item @code {"helo"} >> +@item @code {"mail-from"} >> +@item @code {"rcpt-to"} >> +@item @code {"tag"} >> +@item @code {"tls"} >> +@end itemize >> + >> +@item @code{data} (default @code{#f}) string | @code{> table>} >> +Some options require a string or @code{} to be >> +present. One would specify that table here. >> + >> +@item @code{regex} (default: @code{#f}) boolean >> +Any options using a table may indicate that tables hold regular >> +expressions by setting this option to @code{#t}. >> + >> +@item @code{bool} (default: @code{#t}) boolean >> +When @code{(bool #f)}, this option record is negated. >> +@end itemize >> + >> +@item Data Type: opensmtpd-table >> +This data type represents the configuration of an >> +@code{}. >> + >> +@itemize >> +@item @code{name} (default @code{#f}) >> +@code{name} is the name of the @code{} record. >> + >> +@item @code{data} (default: @code{#f}) string | list | alist | >> nested-list >> +@code{data} expects a string, a list of strings, an alist of >> strings, or >> +a nested list of strings. >> +eg: >> + >> +@itemize >> + >> +@item string >> +@lisp >> +(data "dev@@gnu.org") >> +@end lisp >> + >> +A table of this type is called a @code{string table}. >> + >> +@item list >> +@lisp >> +(data (list ("gnu.org" "fsf.org"))) >> +@end lisp >> + >> +A table of this type is called a @code{list table}. >> + >> +@item alist >> +@lisp >> +(data '(("james" . "$encryptedPassword") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ("jennifer" . "$encryptedPas= sword2))) >> +@end lisp >> + >> +A table of this type is called a @code{mapping table}. >> + >> +@item nested-list >> +@lisp >> +(data '(("user1" "root@@gnu.org" "admin@@gnu.org") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ("user2" "james@@guix.gnu.or= g" "sarah@@fsf.org"))) >> +@end lisp >> + >> +A table of this type is also called a @code{mapping table}. >> + >> +@end itemize >> +@end itemize >> + >> +@item Data Type: opensmtpd-pki >> +This data type represents the configuration of an >> +@code{}. >> + >> +@itemize >> +@item @code{domain} (default @code{#f}) >> +@code{domain} is the string name of the @code{} >> record. >> + >> +@item @code{cert} (default: @code{#f}) >> +@code{cert} (default: @code{#f}) >> + >> +@code{cert} is the string certificate filename to use for this pki. >> + >> +@item @code{key} (default: @code{#f}) >> +@code{key} is the string certificate falename to use for this pki. >> + >> +@item @code{dhe} (default: @code{"none"}) >> +Specify the DHE string parameter to use for DHE cipher suites with >> host >> +pkiname. Valid parameter values are @code{"none"}, @code{"legacy"}, >> or >> +@code{"auto"}. For @code{"legacy"}, a fixed key length of 1024 bits >> is >> +used, whereas for @code{"auto"}, the key length is determined >> +automatically. The default is @code{"none"}, which disables DHE >> cipher >> +suites. >> +@end itemize >> + >> +@item Data Type: opensmtpd-maildir >> +@itemize >> +@item @code{pathname} (default: @code{"~/Maildir"}) >> +Deliver the message to the maildir if pathname if specified, or by >> default >> +to @code{"~/Maildir"}. >> + >> +The pathname may contain format specifiers that are expanded before >> use >> +(see FORMAT SPECIFIERS). >> + >> +@item @code{junk} (default: @code{#f}) >> +If the junk argument is @code{#t}, then the message will be moved to >> the @samp{=E2=80=98Junk=E2=80=99} >> +folder if it contains a positive @samp{=E2=80=98X-Spam=E2=80=99} header= . This folder >> will be >> +created under pathname if it does not yet exist. >> +@end itemize >> + >> +@item Data Type: opensmtpd-mda >> +This record lets you delegate the delivery to a command that >> receives >> +the message on its standard input. >> + >> +@itemize >> +@item @code{name} >> +The string name for this MDA command. >> + >> +@item @code{command} >> +The command to that delivers the mail. >> + >> +The command may contain format specifiers that are expanded before >> use (see >> +FORMAT SPECIFIERS). >> +@end itemize >> + >> +@item Data Type: opensmtpd-queue >> +@itemize >> +@item @code{compression} (default @code{#f}) >> +Store queue files in a compressed format. This may be useful to save >> disk >> +space. >> + >> +@item @code{encryption} (default @code{#f}) >> +Encrypt queue files with EVP@math{_aes}@math{_256}@math{_gcm}(3). If >> no key is specified, it is >> +read with getpass(3). If the string stdin or a single dash (=E2=80=98-= =E2=80=99) is >> given >> +instead of a key, the key is read from the standard input. >> + >> +@item @code{ttl-delay} (default @code{#f}) >> +Set the default expiration time for temporarily undeliverable >> messages, >> +given as a positive decimal integer followed by a unit s, m, h, or >> d. The >> +default is four days (@code{"4d"}). >> +@end itemize >> + >> +@item Data Type: opensmtpd-smtp >> +Data type representing an @code{} record. >> + >> +@itemize >> +@item @code{ciphers} (default: @code{#f}) >> +Set the control string for >> SSL@math{_CTX}@math{_set}@math{_cipher}@math{_list}(3).=C2=A0 The default >> is >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ``HIGH:!aNULL:!MD5''. >> + >> +@item @code{limit-max-mails} (default: @code{100}) >> +Limit the number of messages to count for each sessio >> + >> +@item @code{limit-max-rcpt} (default: @code{1000}) >> +Limit the number of recipients to count for each transaction. >> + >> +@item @code{max-message-size} (default: @code{35M}) >> +Reject messages larger than size, given as a positive number of >> bytes or as >> +a string to be parsed with scan@math{_scaled}(3). >> + >> +@item @code{sub-addr-delim character} (default: @code{+}) >> +When resolving the local part of a local email address, ignore the >> ASCII >> +character and all characters following it. This is helpful for email >> +filters. @samp{"admin+bills@@gnu.org"} is the same email address as >> +@samp{"admin@@gnu.org"}. BUT an email filter can filter emails >> addressed to first >> +email address into a 'Bills' email folder. >> +@end itemize >> + >> +@item Data Type: opensmtpd-srs >> +Use this record to set up the Sender Rewriting Scheme (SRS). >> + >> +@itemize >> +@item @code{key} (default: @code{#f}) >> +Set the secret key to use for SRS, the Sender Rewriting Scheme. >> + >> +@item @code{backup-key} (default: @code{#f}) >> +Set a backup secret key to use as a fallback for SRS@. This can be >> used to >> +implement SRS key rotation. >> + >> +@item @code{ttl-delay} (default: @code{"4d"}) >> +Set the time-to-live delay for SRS envelopes. After this delay, a >> bounce >> +reply to the SRS address will be discarded to limit risks of forged >> +addresses. >> +@end itemize >> + >> +@item Format Specifiers >> +Some configuration records support expansion of their parameters at >> +runtime. Such records (for example >> +@code{}, @code{}) may use >> +format specifiers which are expanded before delivery or relaying. >> The >> +following formats are currently supported: >> + >> +@multitable {aaaaaaaaaaaaaaaaaaa} >> {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} >> +@item @samp{%@{sender@}} >> +@tab sender email address, may be empty string >> +@item @samp{%@{sender.user@}} >> +@tab user part of the sender email address, may be empty >> +@item @samp{%@{sender.domain@}} >> +@tab domain part of the sender email address, may be empty >> +@item @samp{%@{rcpt@}} >> +@tab recipient email address >> +@item @samp{%@{rcpt.user@}} >> +@tab user part of the recipient email address >> +@item @samp{%@{rcpt.domain@}} >> +@tab domain part of the recipient email address >> +@item @samp{%@{dest@}} >> +@tab recipient email address after expansion >> +@item @samp{%@{dest.user@}} >> +@tab user part after expansion >> +@item @samp{%@{dest.domain@}} >> +@tab domain part after expansion >> +@item @samp{%@{user.username@}} >> +@tab local user >> +@item @samp{%@{user.directory@}} >> +@tab home directory of the local user >> +@item @samp{%@{mbox.from@}} >> +@tab name used in mbox From separator lines >> +@item @samp{%@{mda@}} >> +@tab mda command, only available for mda wrappers >> +@end multitable >> + >> +Expansion formats also support partial expansion using the optional >> bracket notations >> +with substring offset.=C2=A0 For example, with recipient domain >> @samp{=E2=80=9Cexample.org=E2=80=9D}: >> + >> +@multitable {aaaaaaaaaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaa} >> +@item @samp{%@{rcpt.domain[0]@}} >> +@tab expands to =E2=80=9Ce=E2=80=9D >> +@item @samp{%@{rcpt.domain[1]@}} >> +@tab expands to =E2=80=9Cx=E2=80=9D >> +@item @samp{%@{rcpt.domain[8:]@}} >> +@tab expands to =E2=80=9Corg=E2=80=9D >> +@item @samp{%@{rcpt.domain[-3:]@}} >> +@tab expands to =E2=80=9Corg=E2=80=9D >> +@item @samp{%@{rcpt.domain[0:6]@}} >> +@tab expands to =E2=80=9Cexample=E2=80=9D >> +@item @samp{%@{rcpt.domain[0:-4]@}} >> +@tab expands to =E2=80=9Cexample=E2=80=9D >> +@end multitable >> + >> +In addition, modifiers may be applied to the token.=C2=A0 For example, >> with recipient >> +@samp{=E2=80=9CUser+Tag@@Example.org=E2=80=9D}: >> + >> +@multitable {aaaaaaaaaaaaaaaaaaaaaaaa} >> {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} >> +@item @samp{%@{rcpt:lowercase@}} >> +@tab expands to =E2=80=9Cuser+tag@@example.org=E2=80=9D >> +@item @samp{%@{rcpt:uppercase@}} >> +@tab expands to =E2=80=9CUSER+TAG@@EXAMPLE.ORG=E2=80=9D >> +@item @samp{%@{rcpt:strip@}} >> +@tab expands to =E2=80=9CUser@@Example.org=E2=80=9D >> +@item @samp{%@{rcpt:lowercasestrip@}} >> +@tab expands to =E2=80=9Cuser@@example.org=E2=80=9D >> +@end multitable >> + >> +For security concerns, expanded values are sanitized and potentially >> dangerous >> +characters are replaced with =E2=80=98:=E2=80=99. In situations where t= hey are >> desirable, the >> +=E2=80=9Craw=E2=80=9D modifier may be applied. For example, with recipi= ent >> +@samp{=E2=80=9Cuser+t?g@@example.org=E2=80=9D}: >> + >> +@multitable {aaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} >> +@item @samp{%@{rcpt@}} >> +@tab expands to =E2=80=9Cuser+t:g@@example.org=E2=80=9D >> +@item @samp{%@{rcpt:raw@}} >> +@tab expands to =E2=80=9Cuser+t?g@@example.org=E2=80=9D >> +@end multitable >> +@end itemize >> + >> =C2=A0@subsubheading Exim Service >> =C2=A0 >> =C2=A0@cindex mail transfer agent (MTA) >> diff --git a/gnu/services/mail.scm b/gnu/services/mail.scm >> index 43f144a42d..4175cab375 100644 >> --- a/gnu/services/mail.scm >> +++ b/gnu/services/mail.scm >> @@ -35,6 +35,10 @@ (define-module (gnu services mail) >> =C2=A0=C2=A0 #:use-module (gnu packages admin) >> =C2=A0=C2=A0 #:use-module (gnu packages dav) >> =C2=A0=C2=A0 #:use-module (gnu packages tls) >> +=C2=A0 #:use-module (guix i18n) >> +=C2=A0 #:use-module (guix diagnostics) >> +=C2=A0 #:use-module (guix ui) >> +=C2=A0 #:use-module (guix utils) >> =C2=A0=C2=A0 #:use-module (guix records) >> =C2=A0=C2=A0 #:use-module (guix packages) >> =C2=A0=C2=A0 #:use-module (guix gexp) >> @@ -58,10 +62,149 @@ (define-module (gnu services mail) >> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= mailbox-configuration >> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= namespace-configuration >> =C2=A0 >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-table >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-table? >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-table-name >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-table-data >> + >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-ca >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-ca? >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-ca-name >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-ca-file >> + >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-pki >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-pki? >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-pki-domain >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-pki-cert >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-pki-key >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-pki-dhe >> + >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-local-delivery >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-local-delivery? >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-local-delivery-method >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-local-delivery-alias >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-local-delivery-ttl >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-local-delivery-user >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-local-delivery-userbase >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-local-delivery-virtual >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-local-delivery-wrapper >> + >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-maildir >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-maildir? >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-maildir-pathname >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-maildir-junk >> + >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-mda >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-mda-name >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-mda-command >> + >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-lmtp >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-lmtp-destination >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-lmtp-rcpt >> + >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-relay >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-relay? >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-relay-name >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-relay-backup >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-relay-backup-mx >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-relay-helo >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-relay-domain >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-relay-host >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-relay-pki >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-relay-srs >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-relay-tls >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-relay-auth >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-relay-mail-from >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-relay-src >> + >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-option >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-option? >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-option-option >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-option-bool >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-option-regex >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-option-data >> + >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-filter-phase >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-filter-phase? >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-filter-phase-name >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-filter-phase-phase >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-filter-phase-options >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-filter-phase-decision >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-filter-phase-message >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-filter-phase-value >> + >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-filter >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-filter? >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-filter-name >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-filter-proc >> + >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface? >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface-interface >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface-family >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface-auth >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface-auth-optional >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface-filters >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface-hostname >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface-hostnames >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface-mask-src >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface-disable-dsn >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface-pki >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface-port >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface-proxy-v2 >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface-received-auth >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface-senders >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface-masquerade >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface-secure-connection >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-interface-tag >> + >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-socket >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-socket? >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-socket-filters >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-socket-mask-src >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-socket-tag >> + >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-match >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-match? >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-match-action >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-match-options >> + >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-smtp >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-smtp? >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-smtp-ciphers >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-smtp-limit-max-mails >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-smtp-limit-max-rcpt >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-smtp-max-message-size >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-smtp-sub-addr-delim character >> + >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-srs >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-srs? >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-srs-key >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-srs-backup-key >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-srs-ttl-delay >> + >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-queue >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-queue? >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-queue-compression >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-queue-encryption >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-queue-ttl-delay >> + >> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= opensmtpd-configuration >> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= opensmtpd-configuration? >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-package >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-config-file >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-configuration-bounce >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-configuration-cas >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-configuration-interfaces >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-configuration-socket >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-configuration-includes >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-configuration-matches >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ;;op= ensmtpd-configuration-mda-wrappers >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-configuration-mta-max-deferred >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-configuration-srs >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-configuration-smtp >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 open= smtpd-configuration-queue >> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= opensmtpd-service-type >> -=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 %def= ault-opensmtpd-config-file >> =C2=A0 >> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= mail-aliases-service-type >> =C2=A0 >> @@ -1641,22 +1784,2351 @@ (define (generate-dovecot-documentation) >> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (listeners unix-listener-conf= iguration fifo-listener- >> configuration >> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 inet-listener-configuration)) >> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (protocol-configuration ,protocol-c= onfiguration-fields)) >> -=C2=A0 'dovecot-configuration)) >> +=C2=A0=C2=A0 'dovecot-configuration)) >> =C2=A0 >> =C2=A0 >> -;;; >> =C2=A0;;; OpenSMTPD. >> =C2=A0;;; >> +;;; This next bit of code helps me create my own sanitizer >> functions. >> + >> +;; some fieldnames have a default value of #f, which is ok.=C2=A0 They >> cannot have >> +;; a value of #t. >> +;; for example opensmtpd-table-data can be #f, BUT NOT true. >> +;; my/sanitize procedure tests values to see if they are of the >> right kind. >> +;; procedure false? is needed to allow fields like 'values' to be >> blank, >> +;; (empty), or #f BUT also have a value like a list of strings. > Use less egocentric comments ;) I'm not sure what you mean here? I know I had a comment in my task list that said something like my sanitizer function are probably better than those fo= und in guix. Apologies for that. > >> +(define (false? var) >> +=C2=A0 (eq? #f var)) >> + >> +;; TODO I have to have this procedure, or I need to change >> my/sanitize >> +;; procedure. >> +(define (my-file-exists? file) >> +=C2=A0 (and (string? file) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (access? file F_OK))) > Does file-exists? not work for you? The file-exists? function causes my-sanitize function to break. I think. If= you get rid of it, then what happens when a user types in (file 4), you get an raise-exception. I can probably just rework my-sanitizer function to deal w= ith that possibility, but I have not yet. I would love some guidance on how to = do that. Because I feel like having to handle that exception is hard. > >> +;; This procedure takes in a var and a list of procedures.=C2=A0 It loo= ps >> through >> +;; list of procedures passing in var to each. >> +;; if one procedure returns #t, the function returns true.=C2=A0 >> Otherwise #f. >> +;; TODO for fun rewrite this using map >> +;; If I rewrote it in map, then it may help with sanitizing. >> +;; eg: I could then potentially easily sanitize vars with lambda >> procedures. >> +(define (is-value-right-type? var list-of-procedures record >> fieldname) >> +=C2=A0 (if (null? list-of-procedures) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 #f >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (if ((car list-of-procedures) var) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 #t >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (is-value-right-= type? var (cdr list-of-procedures) record >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 fieldname)))) > Alternatively, (any (cut <> var) list-of-procedures). You mentioned that in the last review, I just can't figure out how to use y= our suggestion. This is the code that I have in the task list WIP: *** TODO simplify my sanitizing funcions (any (cut <> var)) #+BEGIN_SRC scheme (use-modules (ice-9 curried-definitions) (srfi srfi-26)) (define (((expect-any predicates) record field) var) (if (any (cut <> var) predicates) var (begin ;; code code code ;; how do I tell the user which function failed? (display "error") (throw 'bad! var)))) ;; here is how you use it. (name opensmtpd-table-name ;; string (default #f) (sanitize (lambda (var) (((expect-any (list string? number?)) "hello" "that") v= ar)))) #+END_SRC Does that look close to what you want? I feel like it is way off, but I don= 't know. Honestly when I say this suggestion I was completely blown away, I ha= ve been using (any ) and (every) in a few places to get rid of some uses of primitive eval. > >> +;; converts strings like this: >> +;; "apple, ham, cherry" -> "apple, ham, or cherry" >> +;; "pineapple" -> "pinneapple". >> +;; "cheese, grapefruit, or jam" -> "cheese, grapefruit, or jam" >> +(define (add-comma-or string) >> +=C2=A0 (define last-comma-location (string-rindex string #\,)) >> +=C2=A0 (if last-comma-location >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (if (string-contains string ", or" last-= comma-location) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 string >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (string-replace = string ", or" last-comma-location >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0 (+ 1 last-comma-location))) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 string)) >> + >> + >> +(define (list-of-procedures->string procedures) >> +=C2=A0 (define string >> +=C2=A0=C2=A0=C2=A0 (let loop ((procedures procedures)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (if (null? procedures) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "" >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (begin >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (str= ing-append >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0 (cond ((eq? false? (car procedures)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "#f, ") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? boolean? (car procedures)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "a boolean, ") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? string? (car procedures)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "a string, ") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? integer? (car procedures)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "an integer, ") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? list-of-strings? (car procedu= res)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "a list of strings, ") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? assoc-list? (car procedures)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "an association list of strin= gs, ") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? nested-list? (car procedures)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "a nested-list of strings, ") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? opensmtpd-pki? (car procedure= s)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "an record, ") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? opensmtpd-table? (car procedu= res)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "an record,= ") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? list-of-opensmtpd-match? (car= procedures)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "a list of unique records, ") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? list-of-strings-or-gexps? (ca= r procedures)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "a list of strings or gexps, = ") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ;; TODO can I remove the next two p= rocedures? >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ;; tables-data-are-a* ?=C2=A0 I thi= nk I can. >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? tables-data-are-assoc-list? (= car >> procedures)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (string-append >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "an r= ecord whose fieldname >> 'data' are " >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "an assoc-list.\nFor ex= ample: (opensmtpd-table=C2=A0 >> " >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "(name \"hostnames\") ,= " >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "(data '((\"124.394.23.= 1\" . \"gnu.org\"))))")) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? tables-data-are-a-list-of-str= ings? >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= (car procedures)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (string-append >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "on r= ecord whose fieldname >> 'data' is " >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "a list of strings.\n" >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "For example: (opensmtp= d-table (name >> \"domains\") , " >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "(data (list \"gnu.org\= " \"guix.gnu.org\")))")) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ((eq? my-file-exists? (car procedur= es)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "a file, ") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (else "has an incorrect value, ")) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0 (loop (cdr procedures))))))) >> +=C2=A0 (add-comma-or (string-append (string-drop-right string 2) ".\n")= )) > (define (procedure->string) ...) > (define (procedures->string list) > (define strings (map procedure->string list)) > (string-append > (string-join (drop-right strings 1) ", ") > (if (> (length list) 1) ", or") > (last strings) > ".\n")) > >> +(define (list-of-strings-or-gexps? list) >> +=C2=A0 (and (list? list) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (cond ((null? list) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0 #t) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0 ((or (string? (car list)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (gexp? (car list)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (local-file? (car list)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (file-append? (car list)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (plain-file? (car list)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (computed-file? (car list)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (program-file? (car list))) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0 (list-of-strings-or-gexps? (cdr list))) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0 (else #f)))) >> + >> +(define (my/sanitize var record fieldname list-of-procedures) >> +=C2=A0 (define try-string >> +=C2=A0=C2=A0=C2=A0 (string-append "Try " (list-of-procedures->string li= st-of- >> procedures))) >> +=C2=A0 (if (is-value-right-type? var list-of-procedures record fieldnam= e) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 var >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (begin >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (cond ((string? var) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0 (report-error (G_ "(~a \"~a\") is invalid.~%") >> fieldname var)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0 ((or (number? var) (boolean? var)) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0 (report-error (G_ "(~a ~a) is invalid.~%") fieldname >> var) ) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0 (else >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0 (report-error (G_ "(~a ...) is invalid.~%Value is: >> ~a~%") >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0 fieldname var))) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (display-hint (G_ try-string= )) >> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (throw 'bad! var)))) > This procedure needs a proper name, like sanitize/check-type, but more > importantly, why not simply use define-configuration? Yes! I have slowly been realizing that I have been clumsily re-inventing define-configuration. I hope to switch to define-configuration, because a l= ot of this code would go away. But I need to explore how define-configuration wor= ks. That would be quite a major change. :) > > > Cheers From debbugs-submit-bounces@debbugs.gnu.org Wed Dec 28 15:04:43 2022 Received: (at 56046) by debbugs.gnu.org; 28 Dec 2022 20:04:43 +0000 Received: from localhost ([127.0.0.1]:58772 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pAcfK-0004I2-MK for submit@debbugs.gnu.org; Wed, 28 Dec 2022 15:04:43 -0500 Received: from mail-wm1-f66.google.com ([209.85.128.66]:38639) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pAcfI-0004Hj-JG for 56046@debbugs.gnu.org; Wed, 28 Dec 2022 15:04:41 -0500 Received: by mail-wm1-f66.google.com with SMTP id fm16-20020a05600c0c1000b003d96fb976efso9721512wmb.3 for <56046@debbugs.gnu.org>; Wed, 28 Dec 2022 12:04:40 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=mime-version:user-agent:content-transfer-encoding:references :in-reply-to:date:cc:to:from:subject:message-id:from:to:cc:subject :date:message-id:reply-to; bh=+t2AJ0Ecz3e4yBItYqH/Za3vKi49wDyaIhZX5Jq1Xiw=; b=orhj6lFnqAlhT0QMQYhffRrIrs1is/7RT/psqrr2IWsY7g/EPoCu0VaT2Z1VHtzPvm VZlQNgOPuL5al/gIc8uwX7dd95jswCyEIhaykx0fW1EiJYCOdRUg1doTKXqsEESDkvLx pdOC/4G/rUkgBSxPhVxY5ezt9FW89acqjNf0YS+1L3/L8sthEBXfQxIST7mr+E7dr0kg Dr9suiSSM381/PZnLimqIhRKoaQVqLv5S7p3f99Oeoh+kWMf7H86S4BRkB124spXouVn vdkYXhWVyDOjuDuF3j3/H5IndXGkP/RzgWs3EkS+C8JR2L7dKRSxuVNtb3mWAYNBFEFh oVMQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=mime-version:user-agent:content-transfer-encoding:references :in-reply-to:date:cc:to:from:subject:message-id:x-gm-message-state :from:to:cc:subject:date:message-id:reply-to; bh=+t2AJ0Ecz3e4yBItYqH/Za3vKi49wDyaIhZX5Jq1Xiw=; b=k7k5zcdVYlXR1M1K15kHNBb8cGoHwdgAwAxHYzZ0fRMYCrhBTP0dFhp6xxgh1iTOqB GGp9SZjVx+ukc8lGSjJjFL20GV/Sy340ctFRzIg78IF0iPt1R0lkBlCdSOZvuvFBjUk6 251TaijVXwTqCCtvUjeHMAf4WX/DnY9WJBPDQZHa3OkyxGnoyiaurqb+ucGEoo+ZeqMu IIoaPwfVXk4YgPBXKq3UBa5t02oRh/Rm+4Fj+2qyCznKM6Lrqv5Ue07ZE1cRyk03PAw0 ZECbCWHLpg7OPLlWE4xLDi1tAK3kypr3FKtOuvtaokddGK6jXNHyAjC2ERIK98tmXRMa /iJQ== X-Gm-Message-State: AFqh2koazxuegioRx69KY7Q4teatvQkxvtsSm95kGmRbO4e6RzSrzFPi gi+tg2URPi6aSGeiuzhRjnc= X-Google-Smtp-Source: AMrXdXsInIc7/OuzPLAiloKFvdycWkuHlQg8vHm/ST8hzNt+CqielNzmWPrkF/uSABu+6D5UhaQTlQ== X-Received: by 2002:a05:600c:4e48:b0:3cf:5d41:b748 with SMTP id e8-20020a05600c4e4800b003cf5d41b748mr21652684wmq.36.1672257874730; Wed, 28 Dec 2022 12:04:34 -0800 (PST) Received: from lumine.fritz.box (85-127-52-93.dsl.dynamic.surfer.at. [85.127.52.93]) by smtp.gmail.com with ESMTPSA id t14-20020a05600c198e00b003d990ec74ccsm5494971wmq.46.2022.12.28.12.04.33 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 28 Dec 2022 12:04:34 -0800 (PST) Message-ID: <91b2d3a6834780b36b876dd55699c2941349b5bf.camel@gmail.com> Subject: Re: [PATCH opensmtpd-records v3] services (opensmtpd): add opensmtpd records to enhance opensmtpd-configuration. From: Liliana Marie Prikler To: Joshua Branson Date: Wed, 28 Dec 2022 21:04:32 +0100 In-Reply-To: <87k02co01c.fsf@dismail.de> References: <8001f404c0e54baba71ef9c5536abc54275c4167.1671803496.git.joshua@gnucode.me> <37239abae388f1fadcba559b62c0aae07de9d319.camel@gmail.com> <87k02co01c.fsf@dismail.de> Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable User-Agent: Evolution 3.46.0 MIME-Version: 1.0 X-Spam-Score: 0.0 (/) X-Debbugs-Envelope-To: 56046 Cc: Joshua Branson , 56046@debbugs.gnu.org X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) Am Dienstag, dem 27.12.2022 um 19:16 -0500 schrieb Joshua Branson: > Are you giving me a triple A+ ? :) Org generated the the like that. I > think you mentioned that I should use fractions last time. Sorry I > did not do that. Do you have to convert your documentation from org? Writing Texinfo code manually is an option, as is generating it from define- configuration IIRC. There is also nothing wrong with manually touching up generated docs, but I imagine doing so consistently might be a bit more adventurous. > If I wait 'til I implement every one of your suggestions, I will > probably never submit it. I am really probably "perfecting" this > service. You can submit whatever, but don't expect me or any other committer to upstream the patches while there are open points to address. >=20 > > Instead of a string, take 'no-verify as symbol perhaps? >=20 > Sounds good to me. May I ask why you prefer a symbol instead of a > string? Symbols can be compared with eq?, case et al. > > > -;;; > > > =C2=A0;;; OpenSMTPD. > > > =C2=A0;;; > > > +;;; This next bit of code helps me create my own sanitizer > > > functions. > > > + > > > +;; some fieldnames have a default value of #f, which is ok.=C2=A0 > > > They > > > cannot have > > > +;; a value of #t. > > > +;; for example opensmtpd-table-data can be #f, BUT NOT true. > > > +;; my/sanitize procedure tests values to see if they are of the > > > right kind. > > > +;; procedure false? is needed to allow fields like 'values' to > > > be > > > blank, > > > +;; (empty), or #f BUT also have a value like a list of strings. > > Use less egocentric comments ;) >=20 > I'm not sure what you mean here? I know I had a comment in my task > list that said something like my sanitizer function are probably > better than those found in guix. Apologies for that. For what it's worth, it definitely wasn't I. [1] > >=20 > > > +(define (false? var) > > > +=C2=A0 (eq? #f var)) > > > + > > > +;; TODO I have to have this procedure, or I need to change > > > my/sanitize > > > +;; procedure. > > > +(define (my-file-exists? file) > > > +=C2=A0 (and (string? file) > > > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (access? file F_OK))) > > Does file-exists? not work for you? >=20 > The file-exists? function causes my-sanitize function to break. Why? > I think.=C2=A0 Prove it. > If you get rid of it, then what happens when a user types in (file > 4), you get an raise-exception.=C2=A0 (file-exists? "(file 4)") ; =3D> #f > I can probably just rework my-sanitizer function to > deal with that possibility, but I have not yet. I would love some > guidance on how to do that. Because I feel like having to handle that > exception is hard. >From the Guile manual: -- Scheme Procedure: stat object [exception-on-error?] -- C Function: scm_stat (object, exception_on_error) [...] If the optional EXCEPTION_ON_ERROR argument is true, which is the default, an exception will be raised if the underlying system call returns an error, for example if the file is not found or is not readable. Otherwise, an error will cause =E2=80=98stat=E2=80=99 to re= turn =E2=80=98#f=E2=80=99. Now, in (ice-9 boot-9), file-exists? is defined (assuming posix) as (lambda (str) (->bool (stat str #f))) Thus, I am pretty sure that no exception should be raised from the check ;) > >=20 > > > +;; This procedure takes in a var and a list of procedures.=C2=A0 It > > > loops > > > through > > > +;; list of procedures passing in var to each. > > > +;; if one procedure returns #t, the function returns true.=C2=A0 > > > Otherwise #f. > > > +;; TODO for fun rewrite this using map > > > +;; If I rewrote it in map, then it may help with sanitizing. > > > +;; eg: I could then potentially easily sanitize vars with lambda > > > procedures. > > > +(define (is-value-right-type? var list-of-procedures record > > > fieldname) > > > +=C2=A0 (if (null? list-of-procedures) > > > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 #f > > > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (if ((car list-of-procedures) var) > > > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 #t > > > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (is-value-rig= ht-type? var (cdr list-of-procedures) > > > record > > > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 fieldname)))) > > Alternatively, (any (cut <> var) list-of-procedures). >=20 > You mentioned that in the last review, I just can't figure out how to > use your > suggestion. This is the code that I have in the task list WIP: >=20 > *** TODO simplify my sanitizing funcions=C2=A0 (any (cut <> var)) >=20 > #+BEGIN_SRC scheme > (use-modules (ice-9 curried-definitions) > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 = (srfi srfi-26)) >=20 > (define (((expect-any predicates) record field) var) > =C2=A0 (if (any (cut <> var) predicates) > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 var > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (begin > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ;; code code code > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ;; how do I tell the user whic= h function failed? > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (display "error") > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (throw 'bad! var)))) All of them failed, that's the point. As for constructing a string from a list of procedures, see list-of-procedures->string. > ;; here is how you use it. > =C2=A0 (name opensmtpd-table-name ;; string > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (default #f) > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (sanitize (lambda (var) > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (((expect-any (list string? numb= er?)) "hello" > "that") var)))) >=20 > #+END_SRC >=20 > Does that look close to what you want? I feel like it is way off, but > I don't know. Honestly when I say this suggestion I was completely > blown away, I have been using (any ) and (every) in a few places to > get rid of some uses of primitive eval. I don't see that, but I do see functions that have been dropped still mentioned in the ChangeLog. Another hint at this patch being too convoluted for its own sake ;) > > This procedure needs a proper name, like sanitize/check-type, but > > more importantly, why not simply use define-configuration? >=20 > Yes! I have slowly been realizing that I have been clumsily re- > inventing define-configuration. I hope to switch to define- > configuration, because a lot of this code would go away. But I need > to explore how define-configuration works. > That would be quite a major change. :) Manchmal erspart einem monatelange Implementier-Arbeit einen Nachmittag in der B=C3=BCcherei. Cheers [1] https://issues.guix.gnu.org/issue/56046#4-lineno323 From debbugs-submit-bounces@debbugs.gnu.org Wed Dec 28 15:42:49 2022 Received: (at 56046) by debbugs.gnu.org; 28 Dec 2022 20:42:49 +0000 Received: from localhost ([127.0.0.1]:58794 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pAdGD-0005Oy-5H for submit@debbugs.gnu.org; Wed, 28 Dec 2022 15:42:49 -0500 Received: from mx2.dismail.de ([159.69.191.136]:10650) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1pAdGA-0005Oi-GA for 56046@debbugs.gnu.org; Wed, 28 Dec 2022 15:42:47 -0500 Received: from mx2.dismail.de (localhost [127.0.0.1]) by mx2.dismail.de (OpenSMTPD) with ESMTP id 7dc366de; Wed, 28 Dec 2022 21:42:40 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; d=dismail.de; h= mime-version:date:content-type:content-transfer-encoding:from :message-id:subject:to:cc:in-reply-to:references; s=20190914; bh=1lMazhV2Yb0sViVQHnclqYCkRYd4ZiyVP7KjCTAo+fo=; b=UgOvDUZyDm1z 3tKk7XgaP9kNWtAQCs7Ecj/WH75GasZdCcnuCzmD85513uIRlFTd8JBx7rNDtKEM NFEv/8XN7KMXpfIDYGTAJMIjhapfEYqwx9C9VmXxnC8lAL/zsqq1f6dCMqY2Ib6S +6/ZNmbFhenMgJNfBVpOLup5tmfhkY7MGpVBbJ/K1HKTQDABBzcMO159yz+0Yeun ttUFbdEacQqyONUES7lDPy3s+JgDvvFeLtBJpPvrwkCvX2yDLXsEmAe6KvjzNLHz x7BsJWvyUaUVVd28R8RJygIAcIEwn7M1YzvqEvAA3fomLfqN42OO925/2+SI66IF QD37PS3ANA== Received: from smtp1.dismail.de ( [10.240.26.11]) by mx2.dismail.de (OpenSMTPD) with ESMTP id 4e35cc36; Wed, 28 Dec 2022 21:42:39 +0100 (CET) Received: from smtp1.dismail.de (localhost [127.0.0.1]) by smtp1.dismail.de (OpenSMTPD) with ESMTP id df747f82; Wed, 28 Dec 2022 21:42:39 +0100 (CET) Received: by dismail.de (OpenSMTPD) with ESMTPSA id bc6ac4f7 (TLSv1.3:TLS_AES_256_GCM_SHA384:256:NO); Wed, 28 Dec 2022 21:42:38 +0100 (CET) MIME-Version: 1.0 Date: Wed, 28 Dec 2022 20:42:37 +0000 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable X-Mailer: RainLoop/1.16.0a From: jbranso@dismail.de Message-ID: <69ac260cb40f16a47a6683f11f2b9af0@dismail.de> Subject: Re: [PATCH opensmtpd-records v3] services (opensmtpd): add opensmtpd records to enhance opensmtpd-configuration. To: "Liliana Marie Prikler" In-Reply-To: <91b2d3a6834780b36b876dd55699c2941349b5bf.camel@gmail.com> References: <91b2d3a6834780b36b876dd55699c2941349b5bf.camel@gmail.com> <8001f404c0e54baba71ef9c5536abc54275c4167.1671803496.git.joshua@gnucode.me> <37239abae388f1fadcba559b62c0aae07de9d319.camel@gmail.com> <87k02co01c.fsf@dismail.de> X-Spam-Score: -2.3 (--) X-Debbugs-Envelope-To: 56046 Cc: Joshua Branson , 56046@debbugs.gnu.org X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -3.3 (---) December 28, 2022 3:04 PM, "Liliana Marie Prikler" wrote: > Am Dienstag, dem 27.12.2022 um 19:16 -0500 schrieb Joshua Branson: >=20 >>=20Are you giving me a triple A+ ? :) Org generated the the like that. = I >> think you mentioned that I should use fractions last time. Sorry I >> did not do that. >=20 >=20Do you have to convert your documentation from org? Writing Texinfo > code manually is an option, as is generating it from define- > configuration IIRC. There is also nothing wrong with manually touching > up generated docs, but I imagine doing so consistently might be a bit > more adventurous. I wrote the first draft of the documentation in org. Then converted it to texinfo. I have been writing in texinfo ever since. :) >=20 >>=20If I wait 'til I implement every one of your suggestions, I will >> probably never submit it. I am really probably "perfecting" this >> service. >=20 >=20You can submit whatever, but don't expect me or any other committer t= o > upstream the patches while there are open points to address. Of course. :) I wish I could implement all of your suggestions sooner,= =20 but=20I am still learning. And I might be a bit of a slow coder. :( >> Instead of a string, take 'no-verify as symbol perhaps? >>=20 >>=20Sounds good to me. May I ask why you prefer a symbol instead of a >> string? >=20 >=20Symbols can be compared with eq?, case et al. >=20 >>>=20-;;; >>> ;;; OpenSMTPD. >>> ;;; >>> +;;; This next bit of code helps me create my own sanitizer >>> functions. >>> + >>> +;; some fieldnames have a default value of #f, which is ok.=20 >>>=20They >>> cannot have >>> +;; a value of #t. >>> +;; for example opensmtpd-table-data can be #f, BUT NOT true. >>> +;; my/sanitize procedure tests values to see if they are of the >>> right kind. >>> +;; procedure false? is needed to allow fields like 'values' to >>> be >>> blank, >>> +;; (empty), or #f BUT also have a value like a list of strings. >> Use less egocentric comments ;) >>=20 >>=20I'm not sure what you mean here? I know I had a comment in my task >> list that said something like my sanitizer function are probably >> better than those found in guix. Apologies for that. >=20 >=20For what it's worth, it definitely wasn't I. [1] >=20 >>>=20+(define (false? var) >>> + (eq? #f var)) >>> + >>> +;; TODO I have to have this procedure, or I need to change >>> my/sanitize >>> +;; procedure. >>> +(define (my-file-exists? file) >>> + (and (string? file) >>> + (access? file F_OK))) >> Does file-exists? not work for you? >>=20 >>=20The file-exists? function causes my-sanitize function to break. >=20 >=20Why? >=20 >>=20I think. >=20 >=20Prove it. Oh, the last time I used guile's file-exist? It broke a one of=20 my=20tests. I intend to use guile's file-exist? I just have not figured out how to yet. :) >=20 >>=20If you get rid of it, then what happens when a user types in (file >> 4), you get an raise-exception. >=20 >=20(file-exists? "(file 4)") ; =3D> #f The way I am sanitizing it, something like this happens.=20=20 A=20silly user puts this in their configuration: (opensmtpd-configuration (config-file 4)) (file-exists? (openstmtpd-configuration-config-file record)) (file-exists? 4) =3D> raise-exception >> I can probably just rework my-sanitizer function to >> deal with that possibility, but I have not yet. I would love some >> guidance on how to do that. Because I feel like having to handle that >> exception is hard. >=20 >=20From the Guile manual: >=20 >=20-- Scheme Procedure: stat object [exception-on-error?] > -- C Function: scm_stat (object, exception_on_error) > [...] > If the optional EXCEPTION_ON_ERROR argument is true, which is the > default, an exception will be raised if the underlying system call > returns an error, for example if the file is not found or is not > readable. Otherwise, an error will cause =E2=80=98stat=E2=80=99 to retu= rn =E2=80=98#f=E2=80=99. >=20 >=20Now, in (ice-9 boot-9), file-exists? is defined (assuming posix) as >=20 >=20(lambda (str) > (->bool (stat str #f))) >=20 >=20Thus, I am pretty sure that no exception should be raised from the > check ;) >=20 >>>=20+;; This procedure takes in a var and a list of procedures. It >>> loops >>> through >>> +;; list of procedures passing in var to each. >>> +;; if one procedure returns #t, the function returns true.=20 >>>=20Otherwise #f. >>> +;; TODO for fun rewrite this using map >>> +;; If I rewrote it in map, then it may help with sanitizing. >>> +;; eg: I could then potentially easily sanitize vars with lambda >>> procedures. >>> +(define (is-value-right-type? var list-of-procedures record >>> fieldname) >>> + (if (null? list-of-procedures) >>> + #f >>> + (if ((car list-of-procedures) var) >>> + #t >>> + (is-value-right-type? var (cdr list-of-procedures) >>> record >>> + fieldname)))) >> Alternatively, (any (cut <> var) list-of-procedures). >>=20 >>=20You mentioned that in the last review, I just can't figure out how t= o >> use your >> suggestion. This is the code that I have in the task list WIP: >>=20 >>=20*** TODO simplify my sanitizing funcions (any (cut <> var)) >>=20 >>=20#+BEGIN_SRC scheme >> (use-modules (ice-9 curried-definitions) >> (srfi srfi-26)) >>=20 >>=20(define (((expect-any predicates) record field) var) >> (if (any (cut <> var) predicates) >> var >> (begin >> ;; code code code >> ;; how do I tell the user which function failed? >> (display "error") >> (throw 'bad! var)))) >=20 >=20All of them failed, that's the point. As for constructing a string > from a list of procedures, see list-of-procedures->string. >=20 >>=20;; here is how you use it. >> (name opensmtpd-table-name ;; string >> (default #f) >> (sanitize (lambda (var) >> (((expect-any (list string? number?)) "hello" >> "that") var)))) >>=20 >>=20#+END_SRC >>=20 >>=20Does that look close to what you want? I feel like it is way off, bu= t >> I don't know. Honestly when I say this suggestion I was completely >> blown away, I have been using (any ) and (every) in a few places to >> get rid of some uses of primitive eval. >=20 >=20I don't see that, but I do see functions that have been dropped still > mentioned in the ChangeLog. Another hint at this patch being too > convoluted for its own sake ;) How would I unconvoluted it? Use define-configuration?=20=20 >=20 >> This procedure needs a proper name, like sanitize/check-type, but >> more importantly, why not simply use define-configuration? >>=20 >>=20Yes! I have slowly been realizing that I have been clumsily re- >> inventing define-configuration. I hope to switch to define- >> configuration, because a lot of this code would go away. But I need >> to explore how define-configuration works. >> That would be quite a major change. :) >=20 >=20Manchmal erspart einem monatelange Implementier-Arbeit einen Nachmitt= ag > in der B=C3=BCcherei. >=20 >=20Cheers >=20 >=20[1] https://issues.guix.gnu.org/issue/56046#4-lineno323 From debbugs-submit-bounces@debbugs.gnu.org Thu Jun 15 12:06:16 2023 Received: (at 56046) by debbugs.gnu.org; 15 Jun 2023 16:06:16 +0000 Received: from localhost ([127.0.0.1]:47585 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1q9pUG-0000eq-0D for submit@debbugs.gnu.org; Thu, 15 Jun 2023 12:06:16 -0400 Received: from planete-kraus.eu ([89.234.140.182]:37288) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1q9pUB-0000ee-Rz for 56046@debbugs.gnu.org; Thu, 15 Jun 2023 12:06:14 -0400 Received: from planete-kraus.eu (localhost.lan [127.0.0.1]) by planete-kraus.eu (OpenSMTPD) with ESMTP id 3fdbf7c6 for <56046@debbugs.gnu.org>; Thu, 15 Jun 2023 16:06:09 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha1; c=relaxed; d=planete-kraus.eu; h= message-id:subject:from:to:date:content-type :content-transfer-encoding:mime-version; s=dkim; bh=awXc1VJnXwir sgJ785Lo5KCuW3Y=; b=Dq8gE7cNRuHaf5/TqdEpLoVz2DFJwBCTlV8n7m92Cyj8 U19YcrfnDsESjS2lvV+yu2DBWSDd17cgritdQ3np3mWuYyDchjVE1IBci9nimJKS RhTQR3Yih5NBQOihkZXVvCtlf7W+PGtRGFAZ4+idrEY89DtJf2sf+Etrh9uW4OU= Received: by planete-kraus.eu (OpenSMTPD) with ESMTPSA id 8694b0de (TLSv1.3:TLS_CHACHA20_POLY1305_SHA256:256:NO) for <56046@debbugs.gnu.org>; Thu, 15 Jun 2023 16:06:05 +0000 (UTC) Message-ID: <2016cf7456ca5290d1232f2e47ab18d74e6e3f61.camel@planete-kraus.eu> Subject: Re: [PATCH] services: mail: add opensmtpd records to enhance opensmtpd-configuration. From: Vivien Kraus To: 56046@debbugs.gnu.org Date: Thu, 15 Jun 2023 18:06:01 +0200 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable User-Agent: Evolution 3.46.4 MIME-Version: 1.0 X-Spam-Score: -0.0 (/) X-Debbugs-Envelope-To: 56046 X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) Hello guix, I would very much like to have better support (more modular) for opensmtpd. This patch series seems to go in the right direction. What is its status? Best regards, Vivien From debbugs-submit-bounces@debbugs.gnu.org Fri Aug 18 07:44:38 2023 Received: (at 56046) by debbugs.gnu.org; 18 Aug 2023 11:44:38 +0000 Received: from localhost ([127.0.0.1]:46214 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1qWxu9-0003KA-3W for submit@debbugs.gnu.org; Fri, 18 Aug 2023 07:44:38 -0400 Received: from smtp.gnucode.me ([45.56.66.20]:41018 helo=localhost) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1qWxSs-0008VO-9F for 56046@debbugs.gnu.org; Fri, 18 Aug 2023 07:16:29 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; s=2023-02-28; bh=C5dD5Xw Gv/68k9D0v7mU//GFKgkcPHWjIBzYK65yBWs=; h=in-reply-to:date:references: subject:cc:to:from; d=gnucode.me; b=sKqJn1qTDzrm9rGdndLk2/dcmAhFaMi1q2 hqjXlG1oKfZBvpGqZqik6Pee0xQiQGZ4JEEPMy/p7ASs11zk5yExP6QImKnVrf5WLsUuUH 5XXnFfdAf4zFs+FBgHKIpgk9CbF8KIbraxtsSVXmqF3qUFEjx69t1VRiiBg/EhNI1HA= Received: from localhost (1625265-static.lfytina1.metronetinc.net [104.218.149.227]) by localhost (OpenSMTPD) with ESMTPSA id c0274764 (TLSv1.3:TLS_AES_256_GCM_SHA384:256:NO); Fri, 18 Aug 2023 11:16:22 +0000 (UTC) From: Joshua Branson To: Vivien Kraus Subject: Re: bug#56046: [PATCH] services: mail: add opensmtpd records to enhance opensmtpd-configuration. References: <20220617214618.12377-1-jbranso@dismail.de> <2016cf7456ca5290d1232f2e47ab18d74e6e3f61.camel@planete-kraus.eu> Date: Fri, 18 Aug 2023 07:16:18 -0400 In-Reply-To: <2016cf7456ca5290d1232f2e47ab18d74e6e3f61.camel@planete-kraus.eu> (Vivien Kraus's message of "Thu, 15 Jun 2023 18:06:01 +0200") Message-ID: <87a5uoy519.fsf_-_@dismail.de> User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/28.2 (gnu/linux) MIME-Version: 1.0 Content-Type: text/plain X-Spam-Score: 3.6 (+++) X-Spam-Report: Spam detection software, running on the system "debbugs.gnu.org", has NOT identified this incoming email as spam. The original message has been attached to this so you can view it or label similar future email. If you have any questions, see the administrator of that system for details. Content preview: Vivien Kraus writes: > Hello guix, > > I would very much like to have better support (more modular) for > opensmtpd. This patch series seems to go in the right direction. What > is its status? > > Best regards, > > Vivien Content analysis details: (3.6 points, 10.0 required) pts rule name description ---- ---------------------- -------------------------------------------------- 0.0 FSL_HELO_NON_FQDN_1 No description available. 3.6 HELO_LOCALHOST No description available. 0.0 T_SPF_PERMERROR SPF: test of record failed (permerror) 0.0 KHOP_HELO_FCRDNS Relay HELO differs from its IP's reverse DNS X-Debbugs-Envelope-To: 56046 X-Mailman-Approved-At: Fri, 18 Aug 2023 07:44:34 -0400 Cc: Tobias Geerinckx-Rice , 56046@debbugs.gnu.org X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: 2.6 (++) X-Spam-Report: Spam detection software, running on the system "debbugs.gnu.org", has NOT identified this incoming email as spam. The original message has been attached to this so you can view it or label similar future email. If you have any questions, see the administrator of that system for details. Content preview: Vivien Kraus writes: > Hello guix, > > I would very much like to have better support (more modular) for > opensmtpd. This patch series seems to go in the right direction. What > is its status? > > Best regards, > > Vivien Content analysis details: (2.6 points, 10.0 required) pts rule name description ---- ---------------------- -------------------------------------------------- 0.0 FSL_HELO_NON_FQDN_1 No description available. 3.6 HELO_LOCALHOST No description available. 0.0 T_SPF_PERMERROR SPF: test of record failed (permerror) -1.0 MAILING_LIST_MULTI Multiple indicators imply a widely-seen list manager Vivien Kraus writes: > Hello guix, > > I would very much like to have better support (more modular) for > opensmtpd. This patch series seems to go in the right direction. What > is its status? > > Best regards, > > Vivien So, I recently just got my email server up and running again, which is awesome! What is its status? It works! You can configure fairly basic email configurations. You should try to run it on your server. This blog post walks you through how: https://gnucode.me/submitting-opensmtpd-service-to-guixrus.html You might not be able to use guix deploy with this service at the moment. You might have to ssh into your server and do a "guix system reconfigure". I fixed that bug, but have not pushed it to guixrus yet. Feel free to ask me any questions. In terms of upstreaming this to guix proper. It seems like I should * convert all of the (define-record-type* ...) -> (define-configuration) * make sure all of the tests still work: https://notabug.org/jbranso/guix/src/newOpensmtpdBranch/gnu/tests/mail.scm * try to generate Tobias' smtpd.conf * host a guix system is awesome party There's a ton more in the task here: https://notabug.org/jbranso/linode-guix-system-configuration/src/master/opensmtpd.org Joshua