diff mbox series

[bug#56046,opensmtpd-records,v3] services (opensmtpd): add opensmtpd records to enhance opensmtpd-configuration.

Message ID 8001f404c0e54baba71ef9c5536abc54275c4167.1671803496.git.joshua@gnucode.me
State New
Headers show
Series [bug#56046,opensmtpd-records,v3] services (opensmtpd): add opensmtpd records to enhance opensmtpd-configuration. | expand

Commit Message

Joshua Branson Dec. 23, 2022, 1:52 p.m. UTC
From: Joshua Branson <jbranso@dismail.de>

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(-)

Comments

Liliana Marie Prikler Dec. 26, 2022, 7:34 p.m. UTC | #1
Am Freitag, dem 23.12.2022 um 08:52 -0500 schrieb Joshua Branson:
> 
> * 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.
> 
> * 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}
I hope this will receive proper documentation.
> +@item @strong{listens}
> +@tab @code{<opensmtpd-interface>}
> +@item
> +@tab @code{<opensmtpd-socket>}
> +@item
> +@tab
> +@item @strong{matches}
> +@tab @code{<opensmtpd-match>}
> +@item
> +@tab
> +@item @strong{actions}
> +@tab @code{<opensmtpd-local-delivery>}
> +@item
> +@tab @code{<opensmtpd-relay>}
> +@end multitable
> +
> +Additionally, each @code{<opensmtpd-interface>} and
> +@code{<opensmtpd-socket>} may use a list of @code{<opensmtpd-
> filter>},
> +and/or @code{<opensmtpd-filter-phase>} records to filter
> +email/spam. Also numerous records' fieldnames use
> +@code{<opensmtpd-table>} records to hold lists or key value pairs of
> +data.  Be sure to read the @code{<opensmtpd-table>} section to learn
> the
> +differance between a @code{mapping table} and a @code{list table}.
> +
> +Finally, both @code{<opensmtpd-match>} and
> +@code{<opensmtpd-filter-phase>} use @code{<opensmtpd-option>} 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{<opensmtpd-interface>} 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{<opensmtpd-match>} 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{<opensmtpd-queue>} 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{<opensmtpd-smtp>} 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{<opensmtpd-srs>} 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{<opensmtpd-interface>}. 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{<opensmtpd-interface>} 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{<opensmtpd-filter>} or
> +@code{<opensmtpd-filter-phase>} 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{<opensmtpd-pki>} 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}
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{<opensmtpd-socket>}. 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{<opensmtpd-filter>} or
> +@code{<opensmtpd-filter-phase>} 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{<opensmtpd-match>} 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{<opensmtpd-action>} 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{<opensmtpd-local-delivery>} or
> +@code{<opensmtpd-relay>}.
> +
> +@item @code{options} (default: @code{#f})
> +The fieldname @code{option} is a list of unique
> +@code{<opensmtpd-option>} 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{<opensmtpd-option>}'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_ | <list table>
> +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: <table>), 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_ | <domain>
> +Specify that session may address the string _domain_ or
> +@code{list table} <domain>.
> +
> +@item @code{"for rcpt-to"} _recipient_ | <recipient>
> +Specify that session may address the string _recipient_ or list
> table
> +<recipient>.
> +
> +@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_ | <user>
> +Specify that the session may originate from authenticated _user_ or
> +@code{list table} <user>, 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_ | <sender>
> +Specify that session may originate from _sender_ or @code{list
> table}
> +<sender>, 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_ | <hostname>
> +Specify that session may only originate from an IP address that
> resolves
> +to a reverse DNS matching string _hostname_ or @code{list table}
> +<hostname>.
> +
> +@item @samp{"from socket"}
> +Specify that session may only originate from the local enqueuer.
> +
> +@item @code{"from src"} _address_ | <address>
> +Specify that session may only originate from string _address_ or
> +@code{list table} <address> 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_ | <username>
> +Matches transactions which have been authenticated for string _user_
> or
> +@code{list table} <username>.
> +
> +@item @code{"helo"} _helo-name_ | <helo-name>
> +Specify that session's HELO / EHLO should match the string _helo-
> name_
> +or @code{list table} <helo-name>.
> +
> +@item @code{"mail-from"} _sender_ | <sender>
> +Specify that transactions's MAIL FROM should match the string
> _sender_
> +or @code{list table} <sender>.
> +
> +@item @code{"rcpt-to"} _recipient_ | <recipient>
> +Specify that transaction's RCPT TO should match the string
> _recipient_
> +or @code{list table} <recipient>.
> +
> +@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{<opensmtpd-local-delivery>} 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{<opensmtpd-lmtp>}
> +Deliver the message to an LMTP server at @code{<opensmtpd-lmtp>}'s
> +fieldname @code{destination}. The location may be expressed as
> string
> +host:port or as a UNIX socket. Optionally, @code{<opensmtpd-lmtp>}'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{<opensmtpd-maildir>}
> +Deliver the message to the maildir in
> +@code{<opensmtpd-maildir>}'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{<opensmtpd-maildir>}'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{<opensmtpd-mda>}
> +Delegate the delivery to the @code{<opensmtpd-mda>}'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{<opensmtpd-table>} 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{<opensmtpd-table>} record is used for
> virtual
> +expansion.
> +@end itemize
> +
> +@item Data Type: opensmtpd-relay
> +This data type represents the configuration of an
> +@code{<opensmtpd-relay>} 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{<opensmtpd-table>} 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{<opensmtpd-table>} 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.  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{<opensmtpd-pki>} (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{<opensmtpd-table>}
> +Use the alist @code{<opensmtpd-table>} 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{<opensmtpd-
> table>}
> +Use the string or @code{<opensmtpd-table>} 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{<opensmtpd-filter>}. 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{<opensmtpd-filter-phase>}.
> +
> +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{<opensmtpd-filter-phase>} 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{<opensmtpd-option>} records.
> +
> +At each phase, various options, specified by a list of
> +@code{<opensmtpd-option>}, may be checked. The
> +@code{<opensmtpd-option>}'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{<table>} next to them require
> the
> +@code{<opensmtpd-option>}'s fieldname @code{data} to be an
> +@code{<opensmtpd-table>}. 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"} <table>
> +@tab session has a reverse DNS in table
> +@item @code{"src"} <table>
> +@tab source address is in table
> +@item @code{"helo"} <table>
> +@tab helo name is in table
> +@item @code{"auth"}
> +@tab session is authenticated
> +@item @code{"auth"} <table>
> +@tab session username is in table
> +@item @code{"mail-from"} <table>
> +@tab sender address is in table
> +@item @code{"rcpt-to"} <table>
> +@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{<opensmtpd-option>}'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{<opensmtpd-option>}, which is used by
> +@code{<opensmtpd-filter-phase>} and @code{<opensmtpd-match>}
> +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{<opensmtpd-table>}. When
> the
> +option record is used inside of an @code{<opensmtpd-filter-phase>},
> 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{<opensmtpd-option>} is used inside of an
> +@code{<opensmtpd-match>}, 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{<opensmtpd-
> table>}
> +Some options require a string or @code{<opensmtpd-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 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{<opensmtpd-table>}.
> +
> +@itemize
> +@item @code{name} (default @code{#f})
> +@code{name} is the name of the @code{<opensmtpd-table>} 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{<opensmtpd-pki>}.
> +
> +@itemize
> +@item @code{domain} (default @code{#f})
> +@code{domain} is the string name of the @code{<opensmtpd-pki>}
> 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{<opensmtpd-smtp>} 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{<opensmtpd-maildir>}, @code{<opensmtpd-mda>}) 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.
Use less egocentric comments ;)

> +(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?

> +;; 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))))
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)
> +  (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 <opensmtpd-pki> record, ")
> +                   ((eq? opensmtpd-table? (car procedures))
> +                    "an <opensmtpd-table> record, ")
> +                   ((eq? list-of-opensmtpd-match? (car procedures))
> +                    "a list of unique <opensmtpd-match> 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 <opensmtpd-table> 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 <opensmtpd-table> 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 (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)
> +  (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))))
This procedure needs a proper name, like sanitize/check-type, but more
importantly, why not simply use define-configuration?


Cheers
Joshua Branson Dec. 28, 2022, 12:16 a.m. UTC | #2
Liliana Marie Prikler <liliana.prikler@gmail.com> writes:

> Am Freitag, dem 23.12.2022 um 08:52 -0500 schrieb Joshua Branson:
>> 
>> * 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 email
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.
>> 
>> * 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}
> I hope this will receive proper documentation.

Users can still use

(service opensmtpd-service-type
         (opensmtpd-configuration
           (config-file (local-file "./my-smtpd.conf"))))

config-file is still a fieldname of <opensmtpd-configuration>.  Is that what you mean?

Also the generated documentation is available here:

https://notabug.org/jbranso/linode-guix-system-configuration/src/master/opensmtpd-records-documentation.txt

>> +@item @strong{listens}
>> +@tab @code{<opensmtpd-interface>}
>> +@item
>> +@tab @code{<opensmtpd-socket>}
>> +@item
>> +@tab
>> +@item @strong{matches}
>> +@tab @code{<opensmtpd-match>}
>> +@item
>> +@tab
>> +@item @strong{actions}
>> +@tab @code{<opensmtpd-local-delivery>}
>> +@item
>> +@tab @code{<opensmtpd-relay>}
>> +@end multitable
>> +
>> +Additionally, each @code{<opensmtpd-interface>} and
>> +@code{<opensmtpd-socket>} may use a list of @code{<opensmtpd-
>> filter>},
>> +and/or @code{<opensmtpd-filter-phase>} records to filter
>> +email/spam. Also numerous records' fieldnames use
>> +@code{<opensmtpd-table>} records to hold lists or key value pairs of
>> +data.  Be sure to read the @code{<opensmtpd-table>} section to learn
>> the
>> +differance between a @code{mapping table} and a @code{list table}.
>> +
>> +Finally, both @code{<opensmtpd-match>} and
>> +@code{<opensmtpd-filter-phase>} use @code{<opensmtpd-option>} 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{<opensmtpd-interface>} 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{<opensmtpd-match>} 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{<opensmtpd-queue>} 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{<opensmtpd-smtp>} 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{<opensmtpd-srs>} 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{<opensmtpd-interface>}. 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{<opensmtpd-interface>} 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{<opensmtpd-filter>} or
>> +@code{<opensmtpd-filter-phase>} 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{<opensmtpd-pki>} 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}
> 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{<opensmtpd-socket>}. 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{<opensmtpd-filter>} or
>> +@code{<opensmtpd-filter-phase>} 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{<opensmtpd-match>} 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{<opensmtpd-action>} 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{<opensmtpd-local-delivery>} or
>> +@code{<opensmtpd-relay>}.
>> +
>> +@item @code{options} (default: @code{#f})
>> +The fieldname @code{option} is a list of unique
>> +@code{<opensmtpd-option>} 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 <opensmtpd-option> is it documented like this:

• Data Type: opensmtpd-option This data type represents the
  configuration of an ‘<opensmtpd-option>’, which is used by
  ‘<opensmtpd-filter-phase>’ and ‘<opensmtpd-match>’ 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
<opensmtpd-configuration> 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{<opensmtpd-option>}'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_ | <list table>
>> +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: <table>), 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_ | <domain>
>> +Specify that session may address the string _domain_ or
>> +@code{list table} <domain>.
>> +
>> +@item @code{"for rcpt-to"} _recipient_ | <recipient>
>> +Specify that session may address the string _recipient_ or list
>> table
>> +<recipient>.
>> +
>> +@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_ | <user>
>> +Specify that the session may originate from authenticated _user_ or
>> +@code{list table} <user>, 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_ | <sender>
>> +Specify that session may originate from _sender_ or @code{list
>> table}
>> +<sender>, 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_ | <hostname>
>> +Specify that session may only originate from an IP address that
>> resolves
>> +to a reverse DNS matching string _hostname_ or @code{list table}
>> +<hostname>.
>> +
>> +@item @samp{"from socket"}
>> +Specify that session may only originate from the local enqueuer.
>> +
>> +@item @code{"from src"} _address_ | <address>
>> +Specify that session may only originate from string _address_ or
>> +@code{list table} <address> 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_ | <username>
>> +Matches transactions which have been authenticated for string _user_
>> or
>> +@code{list table} <username>.
>> +
>> +@item @code{"helo"} _helo-name_ | <helo-name>
>> +Specify that session's HELO / EHLO should match the string _helo-
>> name_
>> +or @code{list table} <helo-name>.
>> +
>> +@item @code{"mail-from"} _sender_ | <sender>
>> +Specify that transactions's MAIL FROM should match the string
>> _sender_
>> +or @code{list table} <sender>.
>> +
>> +@item @code{"rcpt-to"} _recipient_ | <recipient>
>> +Specify that transaction's RCPT TO should match the string
>> _recipient_
>> +or @code{list table} <recipient>.
>> +
>> +@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{<opensmtpd-local-delivery>} 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{<opensmtpd-lmtp>}
>> +Deliver the message to an LMTP server at @code{<opensmtpd-lmtp>}'s
>> +fieldname @code{destination}. The location may be expressed as
>> string
>> +host:port or as a UNIX socket. Optionally, @code{<opensmtpd-lmtp>}'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{<opensmtpd-maildir>}
>> +Deliver the message to the maildir in
>> +@code{<opensmtpd-maildir>}'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{<opensmtpd-maildir>}'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{<opensmtpd-mda>}
>> +Delegate the delivery to the @code{<opensmtpd-mda>}'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{<opensmtpd-table>} 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{<opensmtpd-table>} record is used for
>> virtual
>> +expansion.
>> +@end itemize
>> +
>> +@item Data Type: opensmtpd-relay
>> +This data type represents the configuration of an
>> +@code{<opensmtpd-relay>} 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{<opensmtpd-table>} 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{<opensmtpd-table>} 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. If 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.  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{<opensmtpd-pki>} (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{<opensmtpd-table>}
>> +Use the alist @code{<opensmtpd-table>} 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{<opensmtpd-
>> table>}
>> +Use the string or @code{<opensmtpd-table>} 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{<opensmtpd-filter>}. 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{<opensmtpd-filter-phase>}.
>> +
>> +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{<opensmtpd-filter-phase>} 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{<opensmtpd-option>} records.
>> +
>> +At each phase, various options, specified by a list of
>> +@code{<opensmtpd-option>}, may be checked. The
>> +@code{<opensmtpd-option>}'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{<table>} next to them require
>> the
>> +@code{<opensmtpd-option>}'s fieldname @code{data} to be an
>> +@code{<opensmtpd-table>}. 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"} <table>
>> +@tab session has a reverse DNS in table
>> +@item @code{"src"} <table>
>> +@tab source address is in table
>> +@item @code{"helo"} <table>
>> +@tab helo name is in table
>> +@item @code{"auth"}
>> +@tab session is authenticated
>> +@item @code{"auth"} <table>
>> +@tab session username is in table
>> +@item @code{"mail-from"} <table>
>> +@tab sender address is in table
>> +@item @code{"rcpt-to"} <table>
>> +@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{<opensmtpd-option>}'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{<opensmtpd-option>}, which is used by
>> +@code{<opensmtpd-filter-phase>} and @code{<opensmtpd-match>}
>> +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{<opensmtpd-table>}. When
>> the
>> +option record is used inside of an @code{<opensmtpd-filter-phase>},
>> 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{<opensmtpd-option>} is used inside of an
>> +@code{<opensmtpd-match>}, 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{<opensmtpd-
>> table>}
>> +Some options require a string or @code{<opensmtpd-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 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{<opensmtpd-table>}.
>> +
>> +@itemize
>> +@item @code{name} (default @code{#f})
>> +@code{name} is the name of the @code{<opensmtpd-table>} 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{<opensmtpd-pki>}.
>> +
>> +@itemize
>> +@item @code{domain} (default @code{#f})
>> +@code{domain} is the string name of the @code{<opensmtpd-pki>}
>> 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{<opensmtpd-smtp>} 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{<opensmtpd-maildir>}, @code{<opensmtpd-mda>}) 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.
> 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 found
in guix. Apologies for that.

>
>> +(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?

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 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.

>
>> +;; 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))))
> Alternatively, (any (cut <> var) list-of-procedures).

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:

*** 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") var))))

#+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 have
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)
>> +  (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 <opensmtpd-pki> record, ")
>> +                   ((eq? opensmtpd-table? (car procedures))
>> +                    "an <opensmtpd-table> record, ")
>> +                   ((eq? list-of-opensmtpd-match? (car procedures))
>> +                    "a list of unique <opensmtpd-match> 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 <opensmtpd-table> 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 <opensmtpd-table> 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 (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)
>> +  (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))))
> 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 lot of
this code would go away. But I need to explore how define-configuration works.
That would be quite a major change. :)

>
>
> Cheers
Liliana Marie Prikler Dec. 28, 2022, 8:04 p.m. UTC | #3
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.
> 

> > 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?
Symbols can be compared with eq?, case et al.

> > > -;;;
> > >  ;;; 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.
> > 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 found in guix. Apologies for that.
For what it's worth, it definitely wasn't I. [1]

> > 
> > > +(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?
> 
> The file-exists? function causes my-sanitize function to break.
Why?

> I think. 
Prove it.

> If you get rid of it, then what happens when a user types in (file
> 4), you get an raise-exception. 
(file-exists? "(file 4)") ; => #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 ‘stat’ to return ‘#f’.

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 ;)

> > 
> > > +;; 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))))
> > Alternatively, (any (cut <> var) list-of-procedures).
> 
> 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:
> 
> *** 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))))
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.
>   (name opensmtpd-table-name ;; string
>         (default #f)
>         (sanitize (lambda (var)
>                     (((expect-any (list string? number?)) "hello"
> "that") var))))
> 
> #+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 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?
> 
> 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ücherei.

Cheers

[1] https://issues.guix.gnu.org/issue/56046#4-lineno323
ashish.is--- via Guix-patches" via Dec. 28, 2022, 8:42 p.m. UTC | #4
December 28, 2022 3:04 PM, "Liliana Marie Prikler" <liliana.prikler@gmail.com> wrote:

> 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.

I wrote the first draft of the documentation in org.  Then converted it
to texinfo.  I have been writing in texinfo ever since.  :)

> 
>> 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.

Of course.  :)  I wish I could implement all of your suggestions sooner, 
but I am still learning.  And I might be a bit of a slow coder.  :(

>> 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?
> 
> Symbols can be compared with eq?, case et al.
> 
>>> -;;;
>>> ;;; 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.
>> 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 found in guix. Apologies for that.
> 
> For what it's worth, it definitely wasn't I. [1]
> 
>>> +(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?
>> 
>> The file-exists? function causes my-sanitize function to break.
> 
> Why?
> 
>> I think.
> 
> Prove it.

Oh, the last time I used guile's file-exist? It broke a one of 
my tests.  I intend to use guile's file-exist? I just have not
figured out how to yet.  :)

> 
>> If you get rid of it, then what happens when a user types in (file
>> 4), you get an raise-exception.
> 
> (file-exists? "(file 4)") ; => #f

The way I am sanitizing it, something like this happens.  
A silly user puts this in their configuration:

(opensmtpd-configuration
   (config-file 4))


(file-exists? (openstmtpd-configuration-config-file record))

(file-exists? 4)  => 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.
> 
> 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 ‘stat’ to return ‘#f’.
> 
> 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 ;)
> 
>>> +;; 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))))
>> Alternatively, (any (cut <> var) list-of-procedures).
>> 
>> 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:
>> 
>> *** 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))))
> 
> 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.
>> (name opensmtpd-table-name ;; string
>> (default #f)
>> (sanitize (lambda (var)
>> (((expect-any (list string? number?)) "hello"
>> "that") var))))
>> 
>> #+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 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 ;)

How would I unconvoluted it?  Use define-configuration?  

> 
>> 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 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ücherei.
> 
> Cheers
> 
> [1] https://issues.guix.gnu.org/issue/56046#4-lineno323
diff mbox series

Patch

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{<opensmtpd-interface>}
+@item
+@tab @code{<opensmtpd-socket>}
+@item
+@tab
+@item @strong{matches}
+@tab @code{<opensmtpd-match>}
+@item
+@tab
+@item @strong{actions}
+@tab @code{<opensmtpd-local-delivery>}
+@item
+@tab @code{<opensmtpd-relay>}
+@end multitable
+
+Additionally, each @code{<opensmtpd-interface>} and
+@code{<opensmtpd-socket>} may use a list of @code{<opensmtpd-filter>},
+and/or @code{<opensmtpd-filter-phase>} records to filter
+email/spam. Also numerous records' fieldnames use
+@code{<opensmtpd-table>} records to hold lists or key value pairs of
+data.  Be sure to read the @code{<opensmtpd-table>} section to learn the
+differance between a @code{mapping table} and a @code{list table}.
+
+Finally, both @code{<opensmtpd-match>} and
+@code{<opensmtpd-filter-phase>} use @code{<opensmtpd-option>} 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{<opensmtpd-interface>} 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{<opensmtpd-match>} 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{<opensmtpd-queue>} 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{<opensmtpd-smtp>} 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{<opensmtpd-srs>} 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{<opensmtpd-interface>}. 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{<opensmtpd-interface>} 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{<opensmtpd-filter>} or
+@code{<opensmtpd-filter-phase>} 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{<opensmtpd-pki>} 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{<opensmtpd-socket>}. 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{<opensmtpd-filter>} or
+@code{<opensmtpd-filter-phase>} 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{<opensmtpd-match>} 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{<opensmtpd-action>} 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{<opensmtpd-local-delivery>} or
+@code{<opensmtpd-relay>}.
+
+@item @code{options} (default: @code{#f})
+The fieldname @code{option} is a list of unique
+@code{<opensmtpd-option>} 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{<opensmtpd-option>}'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_ | <list table>
+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: <table>), 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_ | <domain>
+Specify that session may address the string _domain_ or
+@code{list table} <domain>.
+
+@item @code{"for rcpt-to"} _recipient_ | <recipient>
+Specify that session may address the string _recipient_ or list table
+<recipient>.
+
+@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_ | <user>
+Specify that the session may originate from authenticated _user_ or
+@code{list table} <user>, 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_ | <sender>
+Specify that session may originate from _sender_ or @code{list table}
+<sender>, 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_ | <hostname>
+Specify that session may only originate from an IP address that resolves
+to a reverse DNS matching string _hostname_ or @code{list table}
+<hostname>.
+
+@item @samp{"from socket"}
+Specify that session may only originate from the local enqueuer.
+
+@item @code{"from src"} _address_ | <address>
+Specify that session may only originate from string _address_ or
+@code{list table} <address> 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_ | <username>
+Matches transactions which have been authenticated for string _user_ or
+@code{list table} <username>.
+
+@item @code{"helo"} _helo-name_ | <helo-name>
+Specify that session's HELO / EHLO should match the string _helo-name_
+or @code{list table} <helo-name>.
+
+@item @code{"mail-from"} _sender_ | <sender>
+Specify that transactions's MAIL FROM should match the string _sender_
+or @code{list table} <sender>.
+
+@item @code{"rcpt-to"} _recipient_ | <recipient>
+Specify that transaction's RCPT TO should match the string _recipient_
+or @code{list table} <recipient>.
+
+@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{<opensmtpd-local-delivery>} 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{<opensmtpd-lmtp>}
+Deliver the message to an LMTP server at @code{<opensmtpd-lmtp>}'s
+fieldname @code{destination}. The location may be expressed as string
+host:port or as a UNIX socket. Optionally, @code{<opensmtpd-lmtp>}'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{<opensmtpd-maildir>}
+Deliver the message to the maildir in
+@code{<opensmtpd-maildir>}'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{<opensmtpd-maildir>}'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{<opensmtpd-mda>}
+Delegate the delivery to the @code{<opensmtpd-mda>}'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{<opensmtpd-table>} 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{<opensmtpd-table>} record is used for virtual
+expansion.
+@end itemize
+
+@item Data Type: opensmtpd-relay
+This data type represents the configuration of an
+@code{<opensmtpd-relay>} 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{<opensmtpd-table>} 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{<opensmtpd-table>} 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{<opensmtpd-pki>} (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{<opensmtpd-table>}
+Use the alist @code{<opensmtpd-table>} 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{<opensmtpd-table>}
+Use the string or @code{<opensmtpd-table>} 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{<opensmtpd-filter>}. 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{<opensmtpd-filter-phase>}.
+
+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{<opensmtpd-filter-phase>} 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{<opensmtpd-option>} records.
+
+At each phase, various options, specified by a list of
+@code{<opensmtpd-option>}, may be checked. The
+@code{<opensmtpd-option>}'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{<table>} next to them require the
+@code{<opensmtpd-option>}'s fieldname @code{data} to be an
+@code{<opensmtpd-table>}. 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"} <table>
+@tab session has a reverse DNS in table
+@item @code{"src"} <table>
+@tab source address is in table
+@item @code{"helo"} <table>
+@tab helo name is in table
+@item @code{"auth"}
+@tab session is authenticated
+@item @code{"auth"} <table>
+@tab session username is in table
+@item @code{"mail-from"} <table>
+@tab sender address is in table
+@item @code{"rcpt-to"} <table>
+@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{<opensmtpd-option>}'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{<opensmtpd-option>}, which is used by
+@code{<opensmtpd-filter-phase>} and @code{<opensmtpd-match>}
+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{<opensmtpd-table>}. When the
+option record is used inside of an @code{<opensmtpd-filter-phase>}, 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{<opensmtpd-option>} is used inside of an
+@code{<opensmtpd-match>}, 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{<opensmtpd-table>}
+Some options require a string or @code{<opensmtpd-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 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{<opensmtpd-table>}.
+
+@itemize
+@item @code{name} (default @code{#f})
+@code{name} is the name of the @code{<opensmtpd-table>} 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{<opensmtpd-pki>}.
+
+@itemize
+@item @code{domain} (default @code{#f})
+@code{domain} is the string name of the @code{<opensmtpd-pki>} 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{<opensmtpd-smtp>} 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{<opensmtpd-maildir>}, @code{<opensmtpd-mda>}) 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 <opensmtpd-pki> record, ")
+                   ((eq? opensmtpd-table? (car procedures))
+                    "an <opensmtpd-table> record, ")
+                   ((eq? list-of-opensmtpd-match? (car procedures))
+                    "a list of unique <opensmtpd-match> 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 <opensmtpd-table> 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 <opensmtpd-table> 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>
+  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>
+  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>
+  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>
+  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>
+  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>
+  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>
+  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>
+  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 "<interface>" or "<table of IP addresses>"
+  ;; 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 <opensmtpd-filter-phase> &
+;; <opensmtpd-match>
+(define-record-type* <opensmtpd-option>
+  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>
+  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>
+  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 <opensmtpd-filter-phase>s
+;; and/or <opensmtpd-filter>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 <opensmtpd-filter> or
+;; <opensmtpd-filter-phase>
+;; returns # otherwise
+(define (opensmtpd-filter-chain? %filters)
+  (and (list-of-unique-filter-or-filter-phase? %filters)
+       (< 1 (length %filters))))
+
+(define-record-type* <opensmtpd-interface>
+  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>
+  opensmtpd-socket make-opensmtpd-socket
+  opensmtpd-socket?
+  ;; false or <opensmtpd-filter> or list of <opensmtpd-filter>
+  (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>
+  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>
+  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>
+  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>
+  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>
   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 "<opensmtpd-configuration> fieldname ")
+                       (display  "'interface' may be #f or a list of records")
+                       (display "\n of unique <opensmtpd-interface>.\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 <opensmtpd-filter> or
+;; <opensmtpd-filter-phase> 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 <opensmtpd-filter-phase>
+       ;; or <opensmtpd-filter>
+       (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 <opensmtpd-interface> and
+;; <opensmtpd-socket> fieldname 'filters'.
+;; It primarily sanitizes <filter-phases>.  The only sanitization it does
+;; for <filter>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 <opensmtpd-filter-phase> 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 <opensmtpd-table> 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 <opensmtpd-table> 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 <opensmtpd-table>
+;; 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 <table>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 <opensmtpd-table>
+        (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 <opensmtpd-table>
+        (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 <opensmtpd-match> 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 <opensmtpd-filter> and
+;; <opensmtpd-filter-phase>. 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
+;; <opensmtpd-filter>, <opensmtpd-filter-phase>, 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 <opensmtpd-configuration>)
+;; to a string.
+;; Consider if a user passed in a valid <opensmtpd-configuration>,
+;; 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 <opensmtpd-filter>s and <opensmtpd-filter-phase>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 <opensmtpd-filter-phase>s and <opensmtpd-filter>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 <opensmtpd-filter>
+                         "")))
+                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 <opensmtpd-action> from
+;; opensmtpd-configuration-matches, which is a list of <opensmtpd-match>.
+;; Each <opensmtpd-match> has a fieldname 'action', which accepts
+;; an <opensmtpd-action>.
+(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 <opensmtpd-configuration> and returns a list whose
+;; elements are <opensmtpd-filter>, <opensmtpd-filter-phase>,
+;; and a filter-chain.
+;; It returns a list of <opensmtpd-filter> and/or <opensmtpd-filter-phase>
+;; 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 <listen-on-record>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 <opensmtpd-interface>
+                   (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 <opensmtpd-table>s assuming the thing you passed into it had
+;; any <opensmtpd-table>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
-    (($ <opensmtpd-configuration> 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
+;    (($ <opensmtpd-configuration> 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
-    (($ <opensmtpd-configuration> 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
+;    (($ <opensmtpd-configuration> 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 <interface> 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 <filter-phase> 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 <filter-phase> 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 <filter-phase> 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 <filter-phase> option 'src' requires a table."
+              #t
+              (opensmtpd-filter-phase
+               (name "filter")
+               (phase "helo")
+               (decision "bypass")
+               (options
+                (list
+                 (opensmtpd-option
+                  (option "src"))))))
+
+  (test-error "Test <filter-phase> 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 <filter-phase> 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 <opensmtpd-option>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 <opensmtp-table> "
+    "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 <opensmtpd-match> 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 <opensmtpd-match> 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 <opensmtpd-table>, 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 <opensmtpd-option>."
+   #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 <opensmtpd-match> is valid.")
+
+  ;; I used to have some code that assumed if you have
+  ;; an "<interface>" and an "<socket>" 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 <opensmtpd-socket> and <opensmtpd-interface>.")
+    (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 <opensmtpd-configuration>."
+    (test-good-record good-opensmtpd-configuration2))
+
+  ;; the matches have two actions with the same name,
+  ;; but are different actions.
+  (test-error
+   (string-append "Test <opensmtpd-configuration> 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 <opensmtpd-socket>.")
+   #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 "<opensmtpd> 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