diff --git a/scripts/run-shellcheck.sh b/scripts/run-shellcheck.sh
index 475f426..d6c2db6 100755
--- a/scripts/run-shellcheck.sh
+++ b/scripts/run-shellcheck.sh
@@ -1,21 +1,29 @@
-#!/bin/sh
+#!/bin/sh -eu
-SHELLCHECKCMD="shellcheck -s sh -f gcc -x"
+SHELLCHECKCMD='shellcheck -s sh -f gcc -x'
# Skip SC2154 for variables starting with __ since such variables are cdist
# environment variables.
SHELLCHECK_SKIP=': __.*is referenced but not assigned.*\[SC2154\]'
-SHELLCHECKTMP=".shellcheck.tmp"
+SHELLCHECKTMP='.shellcheck.tmp'
# Move to top-level cdist-contrib directory.
-cd $(dirname $0)/..
+cd "$(dirname $0)"/..
-check () {
- find type/ -type f $1 $2 -exec ${SHELLCHECKCMD} {} + | grep -v "${SHELLCHECK_SKIP}" > "${SHELLCHECKTMP}"
- test ! -s "${SHELLCHECKTMP}" || { cat "${SHELLCHECKTMP}"; exit 1; }
+check() {
+ find type/ -type f "$@" -exec ${SHELLCHECKCMD} {} + \
+ | grep -v "${SHELLCHECK_SKIP}" >>"${SHELLCHECKTMP}" || true
}
-check -path "*/explorer/*"
-check -path "*/files/*.sh"
+rm -f "${SHELLCHECKTMP}"
+
+check -path '*/explorer/*'
+check -path '*/files/*' -name '*.sh'
check -name manifest
check -name gencode-local
check -name gencode-remote
+
+if test -s "${SHELLCHECKTMP}"
+then
+ cat "${SHELLCHECKTMP}" >&2
+ exit 1
+fi
diff --git a/type/__dma/explorer/auth_conf b/type/__dma/explorer/auth_conf
new file mode 100755
index 0000000..cef0aca
--- /dev/null
+++ b/type/__dma/explorer/auth_conf
@@ -0,0 +1,49 @@
+#!/bin/sh -e
+#
+# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch)
+#
+# This file is part of cdist.
+#
+# cdist is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# cdist is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with cdist. If not, see .
+#
+# This explorer determines the path of dma's auth.conf file
+
+# No dma.conf -> use default
+test -f /etc/dma/dma.conf || {
+ echo /etc/dma/auth.conf
+ exit 0
+}
+test -r /etc/dma/dma.conf || {
+ echo 'Cannot read /etc/dma/dma.conf' >&2
+ exit 1
+}
+
+# Get AUTHPATH from dma.conf
+awk -F'[ \t]' '
+{
+ sub(/#.*$/, "", $0) # remove comments
+ if (!$0) next # ignore empty lines
+}
+$1 == "AUTHPATH" {
+ # Store authpath. In dma conf parsing last wins.
+ if ($2) authpath = substr($0, index($0, " ") + 1)
+}
+END {
+ if (authpath) {
+ print authpath
+ exit 0
+ } else exit 1
+}
+' /etc/dma/dma.conf \
+|| echo /etc/dma/auth.conf # default
diff --git a/type/__dma/explorer/conf b/type/__dma/explorer/conf
new file mode 100755
index 0000000..b4d6d26
--- /dev/null
+++ b/type/__dma/explorer/conf
@@ -0,0 +1,34 @@
+#!/bin/sh -e
+#
+# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch)
+#
+# This file is part of cdist.
+#
+# cdist is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# cdist is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with cdist. If not, see .
+#
+# This explorer returns a sorted list of "active" (= non-commented) lines
+# in the dma.conf file.
+# "Trailing" line comments are stripped off.
+#
+# NOTE: This explorer assumes that the sort(1) utility supports the non-POXIX
+# -s (stable sort) option.
+
+CONF_PATH=/etc/dma # set in Makefile
+dma_conf="${CONF_PATH:?}/dma.conf"
+
+test -f "${dma_conf}" || exit 0
+
+grep -v -e '^[ \t]*#\|^$' "${dma_conf}" \
+| sed -e 's/[ \t]*#.*$//' \
+| sort -s -k 1,1
diff --git a/type/__dma/files/update_dma_conf.awk b/type/__dma/files/update_dma_conf.awk
new file mode 100644
index 0000000..15ef7bf
--- /dev/null
+++ b/type/__dma/files/update_dma_conf.awk
@@ -0,0 +1,178 @@
+#!/usr/bin/awk -f
+#
+# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch)
+#
+# This file is part of cdist.
+#
+# cdist is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# cdist is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with cdist. If not, see .
+
+function comment_line(line) {
+ # returns the position in line at which the comment's text starts
+ # (0 if the line is not a comment)
+ match(line, /^[ \t]*\#+[ \t]*/)
+ return RSTART ? (RLENGTH + 1) : 0
+}
+function empty_line(line) { return line ~ /^[ \t]*$/ }
+function is_word(s) { return s ~ /^[A-Z_]+$/ } # "looks like a plausible word"
+
+function first(line, sep_re) {
+ # returns the part of the line until sep is found
+ # (or the whole line if sep is not found)
+ if (!sep_re) sep_re = "[" SUBSEP "]"
+ match(line, sep_re)
+ return RSTART ? substr(line, 1, RSTART - 1) : line
+}
+
+function rest(line, sep_re) {
+ # returns the part of the line after the first occurrence of sep is found.
+ # (or nothing if sep is not found)
+ if (!sep_re) sep_re = "[" SUBSEP "]"
+ if (match(line, sep_re))
+ return substr(line, RSTART + RLENGTH)
+}
+
+function conf_pop(word, value) {
+ # returns the next value for the config `word` and delete it from the list.
+ # if value is set, this function will only return value if it is the first
+ # option in the list, otherwise it returns 0.
+
+ if (!(word in conf)) return 0
+ if (!value) {
+ if (index(conf[word], SUBSEP)) # more than one element?
+ value = substr(conf[word], 1, index(conf[word], SUBSEP) - 1)
+ else
+ value = conf[word]
+ }
+
+ if (index(conf[word], SUBSEP)) {
+ if (index(conf[word], value SUBSEP) != 1) return 0
+ conf[word] = substr(conf[word], length(value) + 2)
+ } else {
+ if (conf[word] != value) return 0
+ delete conf[word]
+ }
+ return value
+}
+
+function print_conf(word, value) {
+ # print a config line with the given parameters
+ printf "%s", word
+ if (value) printf " %s", value
+ printf "\n"
+}
+
+function print_confs(word, value) {
+ # print config lines for all values stored in conf[word].
+ if (!(word in conf)) return
+ if (conf[word]) {
+ while (value = conf_pop(word))
+ print_conf(word, value)
+ } else {
+ print_conf(word)
+ delete conf[word]
+ }
+}
+
+BEGIN {
+ FS = "\n"
+ EQS = "[ \t]" # copied from dma/conf.c
+
+ if (ARGV[2]) exit (e=1)
+
+ # Loop over file twice!
+ ARGV[2] = ARGV[1]
+ ARGC++
+
+ # read the "should" state into the `conf` array.
+ while (getline < "/dev/stdin") {
+ word = first($0, EQS)
+ if ((word in conf))
+ conf[word] = conf[word] SUBSEP rest($0, EQS)
+ else
+ conf[word] = rest($0, EQS)
+ }
+}
+
+# first pass, gather information about where which information is stored in the
+# current config file. This information will be used in the second pass.
+NR == FNR {
+ if (comment_line($0)) {
+ # comment line
+ word = first(substr($0, comment_line($0)), " ")
+ if (is_word(word)) last_occ["#" word] = FNR
+ } else {
+ word = first($0, EQS)
+ if (is_word(word)) last_occ[word] = FNR
+ }
+}
+
+# before second pass prepare hashes containing location information to be used
+# in the second pass.
+NR > FNR && FNR == 1 {
+ # First we drop the locations of commented-out options if a non-commented
+ # option is available. If a non-commented option is available, we will
+ # append new config options there to have them all at one place.
+ for (k in last_occ)
+ if (k ~ /^\#/ && (substr(k, 2) in last_occ))
+ delete last_occ[k]
+
+ # Reverse the option => line mapping. The line_map allows for easier lookups
+ # in the second pass.
+ for (k in last_occ) line_map[last_occ[k]] = k
+}
+
+# second pass, generate and output new config
+NR > FNR {
+ if (comment_line($0) || empty_line($0)) {
+ # comment or empty line
+ print
+
+ if ((FNR in line_map)) {
+ if (line_map[FNR] ~ /^\#/) {
+ # This line contains a commented config option. If the conf hash
+ # contains options to be set, we output them here because this
+ # option is not used in the current config.
+ k = substr(line_map[FNR], 2)
+ if ((k in conf)) print_confs(k)
+ }
+
+ if (("INSECURE" in conf) && line_map[FNR] ~ /^\#?SECURE$/) {
+ # INSECURE goes where SECURE comment is.
+ print_confs("INSECURE")
+ }
+ }
+ } else {
+ word = first($0, EQS)
+ value = rest($0, EQS)
+ sub(/[ \t]*\#.*$/, "", value) # ignore comments in value
+
+ if ((word in conf) && value == first(conf[word])) {
+ # keep config options we want
+ conf_pop(word)
+ print
+ }
+
+ if ((FNR in line_map) && line_map[FNR] == word) {
+ # rest of config options should be here
+ print_confs(word)
+ }
+ }
+}
+
+END {
+ if (e) exit
+
+ # print rest of config options (
+ for (word in conf) print_confs(word)
+}
diff --git a/type/__dma/gencode-remote b/type/__dma/gencode-remote
new file mode 100755
index 0000000..580b22e
--- /dev/null
+++ b/type/__dma/gencode-remote
@@ -0,0 +1,177 @@
+#!/bin/sh -e
+#
+# 2020 Dennis Camera (dennis.camera@ssrq-sds-fds.ch)
+#
+# This file is part of cdist.
+#
+# cdist is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# cdist is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with cdist. If not, see .
+#
+
+quote() { printf "'%s'" "$(printf '%s' "$*" | sed -e "s/'/'\\\\''/g")"; }
+drop_awk_comments() { quote "$(sed '/^[[:blank:]]*#.*$/d;/^$/d' "$@")"; }
+
+CONF_PATH=/etc/dma # set in Makefile
+
+# Determine mailname
+if test -f "${__object:?}/parameter/mailname"
+then
+ mailname=$(cat "${__object:?}/parameter/mailname")
+else
+ case $(cat "${__global:?}/explorer/os")
+ in
+ (debian|devuan|ubuntu)
+ # On Debian-like systems use /etc/mailname unless --mailname is used
+ mailname='/etc/mailname'
+ ;;
+ (*)
+ mailname=${__target_fqdn:?}
+ ;;
+ esac
+fi
+
+
+# Generate "should" values for config
+conf_should=$(
+ if test -s "${__object:?}/parameter/smarthost"
+ then
+ printf 'SMARTHOST %s\n' "$(cat "${__object:?}/parameter/smarthost")"
+ fi
+
+ printf 'MAILNAME %s\n' "${mailname}"
+
+ if test -s "${__object:?}/explorer/auth_conf"
+ then
+ printf "AUTHPATH %s\n" "$(cat "${__object:?}/explorer/auth_conf")"
+ fi
+
+ case $(cat "${__object:?}/parameter/security")
+ in
+ (ssl|tls)
+ default_smtp_port=465
+ echo 'SECURETRANSFER'
+ ;;
+ (starttls)
+ default_smtp_port=587
+ echo 'SECURETRANSFER'
+ echo 'STARTTLS'
+ ;;
+ (opportunistic)
+ default_smtp_port=25
+ echo 'SECURETRANSFER'
+ echo 'STARTTLS'
+ echo 'OPPORTUNISTIC_TLS'
+ ;;
+ (insecure)
+ default_smtp_port=25
+ echo 'INSECURE'
+ ;;
+ esac
+
+ if test -s "${__object:?}/parameter/port"
+ then
+ printf 'PORT %u\n' "$(cat "${__object:?}/parameter/port")"
+ elif test "${default_smtp_port}" -ne 25 # DMA uses port 25 by default
+ then
+ printf 'PORT %u\n' "${default_smtp_port}"
+ fi
+
+ if test -f "${__object:?}/parameter/masquerade"
+ then
+ while read -r line
+ do
+ printf 'MASQUERADE %s\n' "${line}"
+ done <"${__object:?}/parameter/masquerade"
+ fi
+
+ if test -f "${__object:?}/parameter/defer"
+ then
+ echo 'DEFER'
+ fi
+
+ if test -f "${__object:?}/parameter/fullbounce"
+ then
+ echo 'FULLBOUNCE'
+ fi
+
+ if test -f "${__object:?}/parameter/nullclient"
+ then
+ test -s "${__object:?}/parameter/smarthost" || {
+ echo '--nullclient requires a --smarthost to be defined' >&2
+ exit 1
+ }
+
+ echo 'NULLCLIENT'
+ fi
+)
+# Sort conf_should to compare against "conf_is"
+conf_should=$(echo "${conf_should}" | sort -s -k 1,1)
+
+config_updated=false
+if ! echo "${conf_should}" | cmp -s "${__object:?}/explorer/conf" -
+then
+ # config needs to be updated
+ dma_conf="${CONF_PATH:?}/dma.conf"
+
+ # The following AWK script will output the new config file to be stored on
+ # disk. To do so it reads the current dma.conf file and the config options
+ # that should be set (from stdin).
+ # Note that the path to the current dma.conf is passed to AWK twice, because
+ # the new file cannot be generated in one pass.
+
+ # The logic tries to place options at a sensible location, that is:
+ # a) if the option is already used in the config file:
+ # group all similar options (e.g. MASQUERADE) at one place in the order
+ # they are listed in stdin.
+ # b) if it is a new option and a "default comment" (e.g. "#PORT 25") exists:
+ # place options grouped directly after the comment (the comment is left
+ # alone)
+ # c) otherwise:
+ # options are grouped by word (the first word in the line) and appended
+ # at the end of the file.
+
+ cat <<-CODE
+ awk $(drop_awk_comments "${__type:?}/files/update_dma_conf.awk") $(quote "${dma_conf}") <<'EOF' >$(quote "${dma_conf}.tmp") \
+ && cat $(quote "${dma_conf}.tmp") >$(quote "${dma_conf}")
+ ${conf_should}
+ EOF
+ rm $(quote "${dma_conf}.tmp")
+ CODE
+
+ config_updated=true
+ echo 'config updated' >>"${__messages_out:?}"
+fi
+
+
+# Send a test email if enabled and necessary (=configuration changed)
+if test -f "${__object:?}/parameter/send-test-mail"
+then
+ if grep -q '^__mail_alias/root:' "${__messages_in:?}" \
+ || grep -q '^__dma_auth/' "${__messages_in:?}" \
+ || ${config_updated}
+ then
+ cat <<-CODE
+ sendmail root <<'EOF'
+ Subject: [cdist] Test mail from '${__target_fqdn:?}'
+
+ Hi,
+
+ you can ignore this message.
+ Its sole purpose is to notify you that root mail on ${__target_fqdn:?}
+ will be redirected to you.
+
+ Enjoy!
+ EOF
+ CODE
+ fi
+fi
diff --git a/type/__dma/man.rst b/type/__dma/man.rst
new file mode 100644
index 0000000..29a71fa
--- /dev/null
+++ b/type/__dma/man.rst
@@ -0,0 +1,112 @@
+cdist-type__dma(7)
+============================
+
+NAME
+----
+cdist-type__dma - Setup the DragonFly Mail Agent as the MTA.
+
+
+DESCRIPTION
+-----------
+This (singleton) type uses DMA, a small Mail Transport Agent (MTA), to accept
+mails from locally installed Mail User Agents (MUA) and either deliver the mails
+to a remote smart host for delivery or communicate with remote SMTP servers
+directly.
+
+
+REQUIRED PARAMETERS
+-------------------
+None.
+
+
+BOOLEAN PARAMETERS
+------------------
+defer
+ If enabled, mail will not be sent immediately, but stored in a queue.
+ To flush the queue and send the mails, ```dma -q`` has to be run
+ periodically (e.g. using a cron job.)
+ This type does not manage such a cron job, but some operating systems ship
+ such a cron job with the package.
+fullbounce
+ Enable if bounce messages should include the complete original message,
+ not just the headers.
+nullclient
+ Enable to bypass aliases and local delivery, and instead forward all mails
+ to the defined ``--smarthost``.
+send-test-mail
+ If set, this type will send a test email to root after setup, to check if
+ the configured settings work.
+
+
+OPTIONAL PARAMETERS
+-------------------
+mailname
+ If present, this will be the hostname used to identify this host and the
+ remote part of the sender addresses.
+ If not defined, it defaults to ``/etc/mailname`` on Debian derivatives and
+ to ``__target_fqdn`` otherwise.
+ See `dma(8)` for more information.
+
+ Note: on Debian derivatives the ``/etc/mailname`` file should be updated
+ instead of using this parameter.
+masquerade
+ Masquerade the envelope-from addresses with this address/hostname.
+ Use this setting if mails are not accepted by destination mail servers
+ because your sender domain is invalid.
+ This option can be used multiple times.
+ For more information see the `dma(8)` man page.
+port
+ The port on which to deliver email.
+ If not provided, a sensible default port will be used based on the
+ ``--security`` argument.
+security
+ Configures whether and how DMA should use secure connections.
+
+ ssl/tls
+ Enable TLS/SSL secured transfer.
+ starttls
+ Use STARTTLS to establish a secure connection.
+ opportunistic (default)
+ Will try to establish a secure connection using STARTTLS, but allow
+ unencrypted transfer if STARTTLS fails.
+ Most useful when dma is used without a smarthost, delivering remote
+ messages directly to the outside mail exchangers.
+ insecure
+ allow plain text SMTP login over an insecure connection.
+ Should really *not* be used anymore!
+smarthost
+ The mail server used to send email.
+ It must be configured to act as a relay for the host being configured by
+ this type so that mail can be sent to users non-local to the smarthost.
+
+
+EXAMPLES
+--------
+
+.. code-block:: sh
+
+ # Install DMA and use the smarthost mx1.domain.tld to send mail.
+ __dma --smarthost mx1.domain.tld --send-test-mail
+
+ # Install DMA in a default configuration.
+ __dma
+
+
+SEE ALSO
+--------
+- `DragonFly Mail Agent `_
+- `DragonFly Handbook MTA `_
+
+
+AUTHORS
+-------
+Evilham
+Dennis Camera
+
+
+COPYING
+-------
+Copyright \(C) 2020 Evilham and Dennis Camera. You can redistribute it
+and/or modify it under the terms of the GNU General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
diff --git a/type/__dma/manifest b/type/__dma/manifest
new file mode 100755
index 0000000..530ad09
--- /dev/null
+++ b/type/__dma/manifest
@@ -0,0 +1,66 @@
+#!/bin/sh -e
+#
+# 2020 Dennis Camera (dennis.camera@ssrq-sds-fds.ch)
+#
+# This file is part of cdist.
+#
+# cdist is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# cdist is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with cdist. If not, see .
+#
+
+os=$(cat "${__global:?}/explorer/os")
+
+# Install DMA
+case ${os}
+in
+ (alpine)
+ __package dma --state present
+ export require='__package/dma'
+ ;;
+ (debian|devuan|ubuntu)
+ __package dma --state present
+ export require='__package/dma'
+ ;;
+ (freebsd)
+ # Stop sendmail if necessary
+ __process 'sendmail' --name 'sendmail.*' --state absent \
+ --stop '/etc/rc.d/sendmail onestop'
+
+ # ... and disable it
+ __key_value 'rcconf-sendmail-enable' --file '/etc/rc.conf' \
+ --key 'sendmail_enable' --delimiter '=' --value '"NONE"' \
+ --exact_delimiter
+
+ # Setup mailwrapper accordingly
+ __file '/etc/mail/mailer.conf' --mode 0644 --source - <<-'EOF'
+ #
+ # Execute the "real" sendmail program, named /usr/libexec/sendmail/sendmail
+ #
+ sendmail /usr/libexec/dma
+ send-mail /usr/libexec/dma
+ mailq /usr/libexec/dma
+ newaliases /usr/libexec/dma
+ rmail /usr/libexec/dma
+ EOF
+ ;;
+ (*)
+ cat <&2
+Your OS (${os}) is not supported yet.
+
+Maybe adding support is as simple as adapting the packages or allowing it,
+we highly encourage you to open a PR with the necessary changes.
+See: https://code.ungleich.ch/ungleich-public/cdist-contrib/
+EOF
+ exit 1
+ ;;
+esac
diff --git a/type/__dma/parameter/boolean b/type/__dma/parameter/boolean
new file mode 100644
index 0000000..523bb97
--- /dev/null
+++ b/type/__dma/parameter/boolean
@@ -0,0 +1,4 @@
+defer
+fullbounce
+nullclient
+send-test-mail
diff --git a/type/__dma/parameter/default/security b/type/__dma/parameter/default/security
new file mode 100644
index 0000000..9f1e0a6
--- /dev/null
+++ b/type/__dma/parameter/default/security
@@ -0,0 +1 @@
+opportunistic
diff --git a/type/__dma/parameter/optional b/type/__dma/parameter/optional
new file mode 100644
index 0000000..615c189
--- /dev/null
+++ b/type/__dma/parameter/optional
@@ -0,0 +1,4 @@
+mailname
+port
+security
+smarthost
diff --git a/type/__dma/parameter/optional_multiple b/type/__dma/parameter/optional_multiple
new file mode 100644
index 0000000..70f4146
--- /dev/null
+++ b/type/__dma/parameter/optional_multiple
@@ -0,0 +1 @@
+masquerade
diff --git a/type/__dma/singleton b/type/__dma/singleton
new file mode 100644
index 0000000..e69de29
diff --git a/type/__dma_auth/explorer/auth_conf b/type/__dma_auth/explorer/auth_conf
new file mode 120000
index 0000000..e89de93
--- /dev/null
+++ b/type/__dma_auth/explorer/auth_conf
@@ -0,0 +1 @@
+../../__dma/explorer/auth_conf
\ No newline at end of file
diff --git a/type/__dma_auth/explorer/state b/type/__dma_auth/explorer/state
new file mode 100755
index 0000000..c829cd4
--- /dev/null
+++ b/type/__dma_auth/explorer/state
@@ -0,0 +1,91 @@
+#!/bin/sh -e
+#
+# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch)
+#
+# This file is part of cdist.
+#
+# cdist is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# cdist is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with cdist. If not, see .
+#
+# This explorer looks for a line matching the server parameter
+# in dma's auth.conf and reports:
+# present: a line matching login + host + password exists
+# absent: no line matching login + host exists
+# different_login: a line exists but with a different login user
+# different_password: a line exists but with a different password
+# multiple: multiple lines matching host exist (should not happen)
+
+auth_conf=$("${__type_explorer:?}/auth_conf")
+test -r "${auth_conf}" || exit 0
+
+awk -F'\n' '
+function getvalue(path) {
+ # Reads the first line of the file located at path and returns it.
+ getline < path
+ close(path)
+ return $0
+}
+
+BEGIN {
+ DP = "[: \t]" # copied from dma/conf.c
+
+ parameter_dir = ENVIRON["__object"] "/parameter/"
+
+ # Read the parameters of this object
+ host_param = ENVIRON["__object_id"]
+ login_param = getvalue(parameter_dir "login")
+ passwd_param = getvalue(parameter_dir "password")
+
+ state = "absent"
+}
+
+/^#/ || /^$/ {
+ # skip comments and empty lines
+ next
+}
+
+{
+ # parse line
+
+ login = substr($0, 1, index($0, "|") - 1)
+ if (!login) { login = $0 } # if no "|" found
+
+ host = substr($0, length(login) + 2)
+
+ if (match(host, DP)) {
+ passwd = substr(host, RSTART + 1)
+ host = substr(host, 1, RSTART - 1)
+ } else {
+ passwd = ""
+ }
+}
+
+host == host_param {
+ # a match…
+ if (state == "absent") {
+ if (login != login_param)
+ state = "different_login"
+ else if (passwd != passwd_param)
+ state = "different_password"
+ else
+ state = "present"
+ } else {
+ # report "multiple" to that the type can remove the duplicates.
+ state = "multiple"
+ }
+}
+
+END {
+ print state
+}
+' "${auth_conf}"
diff --git a/type/__dma_auth/files/update_dma_auth.awk b/type/__dma_auth/files/update_dma_auth.awk
new file mode 100644
index 0000000..c50198b
--- /dev/null
+++ b/type/__dma_auth/files/update_dma_auth.awk
@@ -0,0 +1,93 @@
+#!/usr/bin/awk -f
+#
+# 2020 Dennis Camera (dennis.camera@ssrq-sds-fds.ch)
+#
+# This file is part of cdist.
+#
+# cdist is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# cdist is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with cdist. If not, see .
+#
+
+function getvalue(path) {
+ # Reads the first line of the file located at path and returns it.
+ getline < path
+ close(path)
+ return $0
+}
+
+function print_should() {
+ printf "%s|%s:%s\n", login_param, host_param, passwd_param
+}
+
+BEGIN {
+ FS = "\n"
+ DP = "[: \t]" # copied from dma/conf.c
+
+ parameter_dir = ENVIRON["__object"] "/parameter/"
+
+ mode = (getvalue(parameter_dir "state") != "absent")
+
+ host_param = ENVIRON["__object_id"]
+ login_param = getvalue(parameter_dir "login")
+ passwd_param = getvalue(parameter_dir "password")
+}
+
+# skip comments and empty lines
+/^#/ || /^$/ {
+ print
+ next
+}
+
+{
+ # parse line (like dma/conf.c would)
+
+ login = substr($0, 1, index($0, "|") - 1)
+ if (!login) { login = $0 } # if no "|" found
+
+ host = substr($0, length(login) + 2)
+
+ if (match(host, DP)) {
+ passwd = substr(host, RSTART + 1)
+ host = substr(host, 1, RSTART - 1)
+ } else {
+ passwd = ""
+ }
+}
+
+host == host_param {
+ if (mode) {
+ # state_should == present
+ if (!written) {
+ # replace first line if host matches (but only if no line has
+ # been written already -> no duplicates)
+ print_should()
+ written = 1
+ }
+ next
+ } else {
+ # state_should == absent
+ next
+ }
+}
+
+# leave other lines alone
+{
+ print
+}
+
+END {
+ if (mode && !written) {
+ # append line if no match to replace was found
+ print_should()
+ }
+}
diff --git a/type/__dma_auth/gencode-remote b/type/__dma_auth/gencode-remote
new file mode 100755
index 0000000..b6a0100
--- /dev/null
+++ b/type/__dma_auth/gencode-remote
@@ -0,0 +1,72 @@
+#!/bin/sh -e
+#
+# 2020 Dennis Camera (dennis.camera@ssrq-sds-fds.ch)
+#
+# This file is part of cdist.
+#
+# cdist is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# cdist is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with cdist. If not, see .
+#
+
+quote() { printf "'%s'" "$(printf '%s' "$*" | sed -e "s/'/'\\\\''/g")"; }
+drop_awk_comments() { quote "$(sed '/^[[:blank:]]*#.*$/d;/^$/d' "$@")"; }
+
+state_is=$(cat "${__object:?}/explorer/state")
+state_should=$(cat "${__object:?}/parameter/state")
+
+server=${__object_id:?}
+login=$(cat "${__object:?}/parameter/login")
+
+
+auth_conf=$(cat "${__object:?}/explorer/auth_conf")
+test -n "${auth_conf}" || {
+ echo 'Cannot determine path of dma auth.conf' >&2
+ exit 1
+}
+
+if test "${state_is}" = "${state_should}"
+then
+ # state is as it should
+ exit 0
+fi
+
+case ${state_should}
+in
+ (present)
+ test -n "${login}" || { echo '--login must be non-empty' >&2; exit 1; }
+
+ if test "${state_is}" = 'absent'
+ then
+ printf 'add authuser %s on %s\n' "${login}" "${server}" >>"${__messages_out:?}"
+ else
+ printf 'set authuser %s on %s\n' "${login}" "${server}" >>"${__messages_out:?}"
+ fi
+ ;;
+ (absent)
+ printf 'delete authuser %s on %s\n' "${login}" "${server}" >>"${__messages_out:?}"
+ ;;
+ (*)
+ printf 'Invalid --state: %s.\n' "${state_should}" >&2
+ printf 'Acceptable values are: present, absent.\n' >&2
+ exit 1
+ ;;
+esac
+
+
+cat <$(quote "${auth_conf}.tmp") \
+&& cat $(quote "${auth_conf}.tmp") >$(quote "${auth_conf}")
+rm -f $(quote "${auth_conf}.tmp")
+EOF
diff --git a/type/__dma_auth/man.rst b/type/__dma_auth/man.rst
new file mode 100644
index 0000000..da76883
--- /dev/null
+++ b/type/__dma_auth/man.rst
@@ -0,0 +1,66 @@
+cdist-type__dma_auth(7)
+=======================
+
+NAME
+----
+cdist-type__dma_auth - Configure SMTP logins for the DragonFly Mail Agent MTA.
+
+
+DESCRIPTION
+-----------
+This cdist type allows you to set up credentials to log in to remote SMTP
+servers.
+
+NB: dma currently (v0.13) does not differentiate between users on a host.
+ It will use whatever user it finds in the ``auth.conf`` first.
+ Thus, this type will use the ``__object_id`` as the host specifier.
+
+
+REQUIRED PARAMETERS
+-------------------
+login
+ The user's LOGIN name on the SMTP server.
+password
+ The user's password (in plain text.)
+
+
+OPTIONAL PARAMETERS
+-------------------
+state
+ Either ``present`` or ``absent``. Defaults to ``present``.
+
+BOOLEAN PARAMETERS
+------------------
+None.
+
+
+EXAMPLES
+--------
+
+.. code-block:: sh
+
+ # Set the password for smarthost
+ __dma_auth smarthost.example.com --login joe --password hunter2
+
+ # Set credentials for user at an external provider
+ __dma_auth mail.provider.com --login paul@example.com --password letmein
+
+ # Delete credentials for example.com (for all users)
+ __dma_auth example.com --login '' --password '' --state absent
+
+SEE ALSO
+--------
+:strong:`cdist-type__dma`\ (7), :strong:`dma`\ (8)
+
+
+AUTHORS
+-------
+Dennis Camera
+
+
+COPYING
+-------
+Copyright \(C) 2020 Dennis Camera. You can redistribute it
+and/or modify it under the terms of the GNU General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
diff --git a/type/__dma_auth/nonparallel b/type/__dma_auth/nonparallel
new file mode 100644
index 0000000..e69de29
diff --git a/type/__dma_auth/parameter/default/state b/type/__dma_auth/parameter/default/state
new file mode 100644
index 0000000..e7f6134
--- /dev/null
+++ b/type/__dma_auth/parameter/default/state
@@ -0,0 +1 @@
+present
diff --git a/type/__dma_auth/parameter/optional b/type/__dma_auth/parameter/optional
new file mode 100644
index 0000000..ff72b5c
--- /dev/null
+++ b/type/__dma_auth/parameter/optional
@@ -0,0 +1 @@
+state
diff --git a/type/__dma_auth/parameter/required b/type/__dma_auth/parameter/required
new file mode 100644
index 0000000..ae3c622
--- /dev/null
+++ b/type/__dma_auth/parameter/required
@@ -0,0 +1,2 @@
+login
+password
diff --git a/type/__mail_alias/explorer/aliases b/type/__mail_alias/explorer/aliases
new file mode 100755
index 0000000..ac13d7c
--- /dev/null
+++ b/type/__mail_alias/explorer/aliases
@@ -0,0 +1,73 @@
+#!/bin/sh -e
+#
+# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch)
+#
+# This file is part of cdist.
+#
+# cdist is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# cdist is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with cdist. If not, see .
+#
+# Find aliases for a given user name and print the aliases (each one on a
+# separate line)
+
+aliases_file=$("${__type_explorer:?}/aliases_file")
+test -r "${aliases_file}" || exit 0
+
+: "${__object_id:?}" # assert __object_id is set, because it is used in AWK
+
+awk -F ':[ \t]*' '
+function print_aliases(aliases, matches) {
+ # prints comma-separated aliases (one per line)
+ split(aliases, matches, /,[ \t]*/)
+ for (i in matches) {
+ gsub(/^[ \t]*|[ \t]*$/, "", matches[i])
+ if (matches[i]) print matches[i]
+ }
+}
+
+/^#/ {
+ # comment line (ignore)
+ select = 0; cont = 0 # comments terminate alias lists and continuations
+ next
+}
+
+{
+ # is this line a continuation line?
+ # (the prev. line ended in a backslash or the line starts with whitespace)
+ is_cont = /^[ \t]/ || cont
+
+ # detect if the line is a line to be continued (ends with a backslash)
+ cont = /\\$/
+
+ # if it is, we drop the backslash from the line
+ if (cont) sub(/[ \t]*\\$/, "", $0)
+}
+
+is_cont {
+ # if in the alias list of the "target" user, we also print these aliases.
+ if (select) print_aliases($0)
+ next
+}
+
+$1 == ENVIRON["__object_id"] {
+ # "target" user -> print alias list
+ select = 1
+ print_aliases($2)
+ next
+}
+
+{
+ # other user
+ select = 0
+}
+' "${aliases_file}"
diff --git a/type/__mail_alias/explorer/aliases_file b/type/__mail_alias/explorer/aliases_file
new file mode 100755
index 0000000..7f09f88
--- /dev/null
+++ b/type/__mail_alias/explorer/aliases_file
@@ -0,0 +1,52 @@
+#!/bin/sh -e
+#
+# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch)
+#
+# This file is part of cdist.
+#
+# cdist is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# cdist is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with cdist. If not, see .
+#
+# This explorer finds the aliases file to modify.
+
+found() { echo "$*"; exit 0; }
+
+check_file() {
+ if test -f "$1"
+ then
+ found "$1"
+ fi
+}
+
+case $("${__explorer:?}/os")
+in
+ (freebsd|openbsd|solaris)
+ check_file /etc/mail/aliases
+
+ # default
+ found /etc/mail/aliases
+ ;;
+ (alpine|debian|devuan|ubuntu)
+ check_file /etc/aliases
+
+ # default
+ found /etc/aliases
+ ;;
+ (*)
+ check_file /etc/mail/aliases
+ check_file /etc/aliases
+
+ # default
+ found /etc/aliases
+ ;;
+esac
diff --git a/type/__mail_alias/files/update_aliases.awk b/type/__mail_alias/files/update_aliases.awk
new file mode 100644
index 0000000..11a4c85
--- /dev/null
+++ b/type/__mail_alias/files/update_aliases.awk
@@ -0,0 +1,96 @@
+#!/usr/bin/awk -f
+#
+# 2020 Dennis Camera (dennis.camera@ssrq-sds-fds.ch)
+#
+# This file is part of cdist.
+#
+# cdist is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# cdist is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with cdist. If not, see .
+#
+
+function getvalue(path, line) {
+ # Reads the first line of the file located at path and returns it.
+ getline line < path
+ close(path)
+ return line
+}
+
+function sepafter(f, def, _) {
+ # finds the separator between field $f and $(f+1)
+ _ = substr($0, length($f)+1, index(substr($0, length($f)+1), $(f+1))-1)
+ return _ ? _ : def
+}
+
+function write_aliases( line) {
+ if (aliases_written) return
+
+ # print aliases line
+ printf "%s%s", ENVIRON["__object_id"], sepafter(1, ": ")
+ while ((getline line < aliases_should_file) > 0) {
+ if (aliases_written) printf ", "
+ printf "%s", line
+ aliases_written = 1
+ }
+ printf "\n"
+ close(aliases_should_file)
+}
+
+BEGIN {
+ FS = ":[ \t]*"
+
+ parameter_dir = ENVIRON["__object"] "/parameter/"
+
+ mode = (getvalue(parameter_dir "state") != "absent")
+ aliases_should_file = (parameter_dir "/alias")
+}
+
+/^[ \t]*\#/ {
+ # comment line (leave alone)
+ select = 0; cont = 0 # comments terminate alias lists and continuations
+ print
+ next
+}
+
+{
+ # is this line a continuation line?
+ # (the prev. line ended in a backslash or the line starts with whitespace)
+ is_cont = /^[ \t]/ || cont
+
+ # detect if the line is a line to be continued (ends with a backslash)
+ cont = /\\$/
+}
+
+is_cont {
+ # we only print the line if it has not been rewritten (select)
+ if (!select) print
+ next
+}
+
+$1 == ENVIRON["__object_id"] {
+ # "target" user -> rewrite aliases list
+ select = 1
+ if (mode) write_aliases()
+ next
+}
+
+{
+ # other user
+ select = 0
+ print
+}
+
+END {
+ # if the last line was an alias, the separator will be reused (looks better)
+ if (mode && !aliases_written)
+ write_aliases()
+}
diff --git a/type/__mail_alias/gencode-remote b/type/__mail_alias/gencode-remote
new file mode 100755
index 0000000..4a8f889
--- /dev/null
+++ b/type/__mail_alias/gencode-remote
@@ -0,0 +1,87 @@
+#!/bin/sh -e
+#
+# 2020 Dennis Camera (dennis.camera@ssrq-sds-fds.ch)
+#
+# This file is part of cdist.
+#
+# cdist is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# cdist is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with cdist. If not, see .
+#
+
+quote() { printf "'%s'" "$(printf '%s' "$*" | sed -e "s/'/'\\\\''/g")"; }
+drop_awk_comments() { quote "$(sed '/^[[:blank:]]*#.*$/d;/^$/d' "$@")"; }
+
+aliases_file=$(cat "${__object:?}/explorer/aliases_file")
+
+test -n "${aliases_file}" || {
+ echo 'Could not determine aliases file path.' >&2
+ exit 1
+}
+
+
+state_should=$(cat "${__object:?}/parameter/state")
+
+case ${state_should}
+in
+ (present)
+ if cmp -s "${__object:?}/explorer/aliases" "${__object:?}/parameter/alias"
+ then
+ # all good!
+ exit 0
+ fi
+
+ test -s "${__object:?}/parameter/alias" || {
+ printf 'The --alias parameter is required if --state present.\n' >&2
+ printf 'Use --state absent to remove all aliases.\n' >&2
+ exit 1
+ }
+
+ if test -s "${__object:?}/explorer/aliases"
+ then
+ echo "update aliases" >>"${__messages_out:?}"
+ else
+ echo "add aliases" >>"${__messages_out:?}"
+ fi
+ ;;
+ (absent)
+ # nothing to do if no aliases found.
+ test -s "${__object:?}/explorer/aliases" || exit 0
+
+ echo "delete aliases" >>"${__messages_out:?}"
+ ;;
+ (*)
+ printf 'Invalid --state: %s.\n' "${state_should}" >&2
+ printf 'Acceptable values are: present, absent.\n' >&2
+ exit 1
+esac
+
+cat <$(quote "${aliases_file}.tmp") \
+|| {
+ rm -f $(quote "${aliases_file}.tmp")
+ echo 'Generating new aliases file failed!' >&2
+ exit 1
+}
+
+if ! cmp -s $(quote "${aliases_file}") $(quote "${aliases_file}.tmp")
+then
+ # aliases file was modified, replace:
+ cat $(quote "${aliases_file}.tmp") >$(quote "${aliases_file}")
+
+ # then, run newaliases if present ("missing" on Alpine Linux because of typo)
+ command -v newaliases >/dev/null 2>&1 && newaliases || true
+fi
+rm -f $(quote "${aliases_file}.tmp")
+EOF
diff --git a/type/__mail_alias/man.rst b/type/__mail_alias/man.rst
new file mode 100644
index 0000000..de40512
--- /dev/null
+++ b/type/__mail_alias/man.rst
@@ -0,0 +1,76 @@
+cdist-type__mail_alias(7)
+=========================
+
+NAME
+----
+cdist-type__mail_alias - Manage mail aliases.
+
+
+DESCRIPTION
+-----------
+This cdist type allows you to configure mail aliases (/etc/aliases).
+
+
+REQUIRED PARAMETERS
+-------------------
+None.
+
+
+OPTIONAL PARAMETERS
+-------------------
+state
+ 'present' or 'absent', defaults to 'present'
+alias
+ an alias, i.e. a mail address where mail for the user should be redirected
+ to.
+ This parameter can be specified multiple times to redirect to multiple
+ recipients.
+ If ``--state`` is ``present`` this parameter is required.
+ See `aliases(5)` for the different forms this parameter can take.
+
+
+BOOLEAN PARAMETERS
+------------------
+None.
+
+
+EXAMPLES
+--------
+
+.. code-block:: sh
+
+ # Redirect root mail to a "real" email address
+ __mail_alias root --alias admin@example.com
+
+ # Disable redirection of mail for joe
+ __mail_alias joe --state absent
+
+
+BUGS
+----
+- Quoted strings are not parsed by this type. As a result, aliases
+ containing ``,`` (commas) are treated incorrectly (they are treated as
+ separate aliases.)
+ Make sure that email addresses, file names, and pipe commands do not contain
+ commas.
+- ``:include:`` directives in the aliases file are not evaluated by this type.
+ They are treated like a regular alias, the values of the included file are
+ not expanded.
+
+
+SEE ALSO
+--------
+:strong:`aliases`\ (5)
+
+
+AUTHORS
+-------
+Dennis Camera
+
+
+COPYING
+-------
+Copyright \(C) 2020 Dennis Camera. You can redistribute it
+and/or modify it under the terms of the GNU General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
diff --git a/type/__mail_alias/nonparallel b/type/__mail_alias/nonparallel
new file mode 100644
index 0000000..e69de29
diff --git a/type/__mail_alias/parameter/default/state b/type/__mail_alias/parameter/default/state
new file mode 100644
index 0000000..e7f6134
--- /dev/null
+++ b/type/__mail_alias/parameter/default/state
@@ -0,0 +1 @@
+present
diff --git a/type/__mail_alias/parameter/optional b/type/__mail_alias/parameter/optional
new file mode 100644
index 0000000..ff72b5c
--- /dev/null
+++ b/type/__mail_alias/parameter/optional
@@ -0,0 +1 @@
+state
diff --git a/type/__mail_alias/parameter/optional_multiple b/type/__mail_alias/parameter/optional_multiple
new file mode 100644
index 0000000..d077ed8
--- /dev/null
+++ b/type/__mail_alias/parameter/optional_multiple
@@ -0,0 +1 @@
+alias