mirror of
https://github.com/docker-mailserver/docker-mailserver.git
synced 2025-05-21 23:43:06 +02:00
Refactoring this `setup` CLI command as part of the effort to unify our DKIM feature support between OpenDKIM + Rspamd: - Adds a `main()` method similar to other setup CLI commands. - Help text more aligned with equivalent rspamd DKIM setup CLI command. - DRY some repetition such as hard-coded paths to use variables. - OpenDKIM config files are created / initialized early on now with `_create_opendkim_configs()`. `while` loop only needs to append entries, so is easier to grok. - `_create_dkim_key()` to scope just the logic (_and additional notes_) to key generation via `opendkim-genkey` - Now overall logic with the `while` loop of the script occurs in `_generate_dkim_keys()`: - Ownership fixes are now applied after the `while` loop as that seems more appropriate than per iteration. - Temporary VHOST config is now removed since it's no longer useful after running. - Tests adjusted for one new log for adding of default trusted hosts content. Overall this should be nicer to grok/maintain. Some of this logic will be reused for the unified DKIM generation command in future, which is more likely to shift towards all domains using the same keypair by default with rspamd/opendkim config generated at runtime rather than reliant upon DMS config volume to provide that (_still expected for private key_). --------- Co-authored-by: Casper <casperklein@users.noreply.github.com> Co-authored-by: Georg Lauterbach <44545919+georglauterbach@users.noreply.github.com>
438 lines
14 KiB
Bash
438 lines
14 KiB
Bash
#!/bin/bash
|
|
|
|
# ? ABOUT: Functions defined here aid with common functionality during tests.
|
|
|
|
# ! ATTENTION: Functions prefixed with `__` are intended for internal use within this file only, not in tests.
|
|
|
|
# ! -------------------------------------------------------------------
|
|
# ? >> Miscellaneous initialization functionality
|
|
|
|
# shellcheck disable=SC2155
|
|
|
|
# Load additional BATS libraries for more functionality.
|
|
#
|
|
# ## Note
|
|
#
|
|
# This function is internal and should not be used in tests.
|
|
function __load_bats_helper() {
|
|
load "${REPOSITORY_ROOT}/test/test_helper/bats-support/load"
|
|
load "${REPOSITORY_ROOT}/test/test_helper/bats-assert/load"
|
|
load "${REPOSITORY_ROOT}/test/helper/sending"
|
|
load "${REPOSITORY_ROOT}/test/helper/log_and_filtering"
|
|
}
|
|
|
|
__load_bats_helper
|
|
|
|
# Properly handle the container name given to tests. This makes the whole
|
|
# test suite more robust as we can be sure that the container name is
|
|
# properly set. Sometimes, we need to provide an explicit container name;
|
|
# this function eases the pain by either providing the explicitly given
|
|
# name or `CONTAINER_NAME` if it is set.
|
|
#
|
|
# @param ${1} = explicit container name [OPTIONAL]
|
|
#
|
|
# ## Attention
|
|
#
|
|
# Note that this function checks whether the name given to it starts with
|
|
# the prefix `dms-test_`. One must adhere to this naming convention.
|
|
#
|
|
# ## Panics
|
|
#
|
|
# If neither an explicit non-empty argument is given nor `CONTAINER_NAME`
|
|
# is set.
|
|
#
|
|
# ## "Calling Convention"
|
|
#
|
|
# This function should be called the following way:
|
|
#
|
|
# local SOME_VAR=$(__handle_container_name "${X:-}")
|
|
#
|
|
# Where `X` is an arbitrary argument of the function you're calling.
|
|
#
|
|
# ## Note
|
|
#
|
|
# This function is internal and should not be used in tests.
|
|
function __handle_container_name() {
|
|
if [[ -n ${1:-} ]] && [[ ${1:-} =~ ^dms-test_ ]]; then
|
|
printf '%s' "${1}"
|
|
return 0
|
|
elif [[ -n ${CONTAINER_NAME:-} ]]; then
|
|
printf '%s' "${CONTAINER_NAME}"
|
|
return 0
|
|
else
|
|
echo 'ERROR: (helper/common.sh) Container name was either provided explicitly without the required "dms-test_" prefix, or CONTAINER_NAME is not set for implicit usage' >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# ? << Miscellaneous initialization functionality
|
|
# ! -------------------------------------------------------------------
|
|
# ? >> Functions to execute commands inside a container
|
|
|
|
|
|
# Execute a command inside a container with an explicit name.
|
|
#
|
|
# @param ${1} = container name
|
|
# @param ... = command to execute
|
|
function _exec_in_container_explicit() {
|
|
local CONTAINER_NAME=${1:?Container name must be provided when using explicit}
|
|
shift 1
|
|
docker exec "${CONTAINER_NAME}" "${@}"
|
|
}
|
|
|
|
# Execute a command inside the container with name ${CONTAINER_NAME}.
|
|
#
|
|
# @param ... = command to execute
|
|
function _exec_in_container() {
|
|
_exec_in_container_explicit "${CONTAINER_NAME:?Container name must be provided}" "${@}"
|
|
}
|
|
|
|
# Execute a command inside a container with an explicit name. The command is run with
|
|
# BATS' `run` so you can check the exit code and use `assert_`.
|
|
#
|
|
# @param ${1} = container name
|
|
# @param ... = command to execute
|
|
function _run_in_container_explicit() {
|
|
local CONTAINER_NAME=${1:?Container name must be provided when using explicit}
|
|
shift 1
|
|
run _exec_in_container_explicit "${CONTAINER_NAME}" "${@}"
|
|
}
|
|
|
|
# Execute a command inside the container with name ${CONTAINER_NAME}. The command
|
|
# is run with BATS' `run` so you can check the exit code and use `assert_`.
|
|
#
|
|
# @param ... = command to execute
|
|
function _run_in_container() {
|
|
_run_in_container_explicit "${CONTAINER_NAME:?Container name must be provided}" "${@}"
|
|
}
|
|
|
|
# Execute a command inside the container with name ${CONTAINER_NAME}. Moreover,
|
|
# the command is run by Bash with `/bin/bash -c`.
|
|
#
|
|
# @param ... = command to execute with Bash
|
|
function _exec_in_container_bash() { _exec_in_container /bin/bash -c "${@}" ; }
|
|
|
|
# Execute a command inside the container with name ${CONTAINER_NAME}. The command
|
|
# is run with BATS' `run` so you can check the exit code and use `assert_`. Moreover,
|
|
# the command is run by Bash with `/bin/bash -c`.
|
|
#
|
|
# @param ... = Bash command to execute
|
|
function _run_in_container_bash() { _run_in_container /bin/bash -c "${@}" ; }
|
|
|
|
# Run a command in Bash and filter the output given a regex.
|
|
#
|
|
# @param ${1} = command to run in Bash
|
|
# @param ${2} = regex to filter [OPTIONAL]
|
|
#
|
|
# ## Attention
|
|
#
|
|
# The regex is given to `grep -E`, so make sure it is compatible.
|
|
#
|
|
# ## Note
|
|
#
|
|
# If no regex is provided, this function will default to one that strips
|
|
# empty lines and Bash comments from the output.
|
|
function _run_in_container_bash_and_filter_output() {
|
|
local COMMAND=${1:?Command must be provided}
|
|
local FILTER_REGEX=${2:-^[[:space:]]*$|^ *#}
|
|
|
|
_run_in_container_bash "${COMMAND} | grep -E -v '${FILTER_REGEX}'"
|
|
assert_success
|
|
}
|
|
|
|
# ? << Functions to execute commands inside a container
|
|
# ! -------------------------------------------------------------------
|
|
# ? >> Functions about executing commands with timeouts
|
|
|
|
# Repeats a given command inside a container until the timeout is over.
|
|
#
|
|
# @param ${1} = timeout
|
|
# @param ${2} = container name
|
|
# @param ... = test command for container
|
|
function _repeat_in_container_until_success_or_timeout() {
|
|
local TIMEOUT="${1:?Timeout duration must be provided}"
|
|
local CONTAINER_NAME="${2:?Container name must be provided}"
|
|
shift 2
|
|
|
|
_repeat_until_success_or_timeout \
|
|
--fatal-test "_container_is_running ${CONTAINER_NAME}" \
|
|
"${TIMEOUT}" \
|
|
_exec_in_container "${@}"
|
|
}
|
|
|
|
# Repeats a given command until the timeout is over.
|
|
#
|
|
# @option --fatal-test <COMMAND EVAL STRING> = additional test whose failure aborts immediately
|
|
# @param ${1} = timeout
|
|
# @param ... = test to run
|
|
function _repeat_until_success_or_timeout() {
|
|
local FATAL_FAILURE_TEST_COMMAND
|
|
|
|
if [[ "${1:-}" == "--fatal-test" ]]; then
|
|
FATAL_FAILURE_TEST_COMMAND="${2:?Provided --fatal-test but no command}"
|
|
shift 2
|
|
fi
|
|
|
|
local TIMEOUT=${1:?Timeout duration must be provided}
|
|
shift 1
|
|
|
|
if ! [[ "${TIMEOUT}" =~ ^[0-9]+$ ]]; then
|
|
echo "First parameter for timeout must be an integer, received \"${TIMEOUT}\""
|
|
return 1
|
|
fi
|
|
|
|
local STARTTIME=${SECONDS}
|
|
|
|
until "${@}"; do
|
|
if [[ -n ${FATAL_FAILURE_TEST_COMMAND} ]] && ! eval "${FATAL_FAILURE_TEST_COMMAND}"; then
|
|
echo "\`${FATAL_FAILURE_TEST_COMMAND}\` failed, early aborting repeat_until_success of \`${*}\`" >&2
|
|
return 1
|
|
fi
|
|
|
|
sleep 1
|
|
|
|
if [[ $(( SECONDS - STARTTIME )) -gt ${TIMEOUT} ]]; then
|
|
echo "Timed out on command: ${*}" >&2
|
|
return 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Like `_repeat_until_success_or_timeout` . The command is run with BATS' `run`
|
|
# so you can check the exit code and use `assert_`.
|
|
#
|
|
# @param ${1} = timeout
|
|
# @param ... = test command to run
|
|
function _run_until_success_or_timeout() {
|
|
local TIMEOUT=${1:?Timeout duration must be provided}
|
|
shift 1
|
|
|
|
if [[ ! ${TIMEOUT} =~ ^[0-9]+$ ]]; then
|
|
echo "First parameter for timeout must be an integer, received \"${TIMEOUT}\""
|
|
return 1
|
|
fi
|
|
|
|
local STARTTIME=${SECONDS}
|
|
|
|
# shellcheck disable=SC2154
|
|
until run "${@}" && [[ ${status} -eq 0 ]]; do
|
|
sleep 1
|
|
|
|
if (( SECONDS - STARTTIME > TIMEOUT )); then
|
|
echo "Timed out on command: ${*}" >&2
|
|
return 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
# ? << Functions about executing commands with timeouts
|
|
# ! -------------------------------------------------------------------
|
|
# ? >> Functions to wait until a condition is met
|
|
|
|
# Wait until a port is ready.
|
|
#
|
|
# @param ${1} = port
|
|
# @param ${2} = container name [OPTIONAL]
|
|
function _wait_for_tcp_port_in_container() {
|
|
local PORT=${1:?Port number must be provided}
|
|
local CONTAINER_NAME=$(__handle_container_name "${2:-}")
|
|
|
|
_repeat_until_success_or_timeout \
|
|
--fatal-test "_container_is_running ${CONTAINER_NAME}" \
|
|
"${TEST_TIMEOUT_IN_SECONDS}" \
|
|
_exec_in_container_bash "nc -z 0.0.0.0 ${PORT}"
|
|
}
|
|
|
|
# Wait for SMTP port (25) to become ready.
|
|
#
|
|
# @param ${1} = name of the container [OPTIONAL]
|
|
function _wait_for_smtp_port_in_container() {
|
|
local CONTAINER_NAME=$(__handle_container_name "${1:-}")
|
|
_wait_for_tcp_port_in_container 25
|
|
}
|
|
|
|
# Wait until the SMTP port (25) can respond.
|
|
#
|
|
# @param ${1} = name of the container [OPTIONAL]
|
|
function _wait_for_smtp_port_in_container_to_respond() {
|
|
local CONTAINER_NAME=$(__handle_container_name "${1:-}")
|
|
|
|
local COUNT=0
|
|
until [[ $(_exec_in_container timeout 10 /bin/bash -c 'echo QUIT | nc localhost 25') == *'221 2.0.0 Bye'* ]]; do
|
|
if [[ ${COUNT} -eq 20 ]]; then
|
|
echo "Unable to receive a valid response from 'nc localhost 25' within 20 seconds"
|
|
return 1
|
|
fi
|
|
|
|
sleep 1
|
|
(( COUNT += 1 ))
|
|
done
|
|
}
|
|
|
|
# Checks whether a service is running inside a container (${1}).
|
|
#
|
|
# @param ${1} = service name
|
|
# @param ${2} = container name [OPTIONAL]
|
|
function _should_have_service_running_in_container() {
|
|
local SERVICE_NAME="${1:?Service name must be provided}"
|
|
local CONTAINER_NAME=$(__handle_container_name "${2:-}")
|
|
|
|
_run_in_container /usr/bin/supervisorctl status "${SERVICE_NAME}"
|
|
assert_success
|
|
assert_output --partial 'RUNNING'
|
|
}
|
|
|
|
# Wait until a service is running.
|
|
#
|
|
# @param ${1} = name of the service to wait for
|
|
# @param ${2} = container name [OPTIONAL]
|
|
function _wait_for_service() {
|
|
local SERVICE_NAME="${1:?Service name must be provided}"
|
|
local CONTAINER_NAME=$(__handle_container_name "${2:-}")
|
|
|
|
_repeat_until_success_or_timeout \
|
|
--fatal-test "_container_is_running ${CONTAINER_NAME}" \
|
|
"${TEST_TIMEOUT_IN_SECONDS}" \
|
|
_should_have_service_running_in_container "${SERVICE_NAME}"
|
|
}
|
|
|
|
# An account added to `postfix-accounts.cf` must wait for the `changedetector` service
|
|
# to process the update before Dovecot creates the mail account and associated storage dir.
|
|
#
|
|
# @param ${1} = mail account name
|
|
# @param ${2} = container name
|
|
function _wait_until_account_maildir_exists() {
|
|
local MAIL_ACCOUNT=${1:?Mail account must be provided}
|
|
local CONTAINER_NAME=$(__handle_container_name "${2:-}")
|
|
|
|
local LOCAL_PART="${MAIL_ACCOUNT%@*}"
|
|
local DOMAIN_PART="${MAIL_ACCOUNT#*@}"
|
|
local MAIL_ACCOUNT_STORAGE_DIR="/var/mail/${DOMAIN_PART}/${LOCAL_PART}"
|
|
|
|
_repeat_in_container_until_success_or_timeout 60 "${CONTAINER_NAME}" \
|
|
/bin/bash -c "[[ -d ${MAIL_ACCOUNT_STORAGE_DIR} ]]"
|
|
}
|
|
|
|
# Wait until the mail queue is empty inside a container (${1}).
|
|
#
|
|
# @param ${1} = container name [OPTIONAL]
|
|
function _wait_for_empty_mail_queue_in_container() {
|
|
local CONTAINER_NAME=$(__handle_container_name "${1:-}")
|
|
local TIMEOUT=${TEST_TIMEOUT_IN_SECONDS}
|
|
|
|
# shellcheck disable=SC2016
|
|
_repeat_in_container_until_success_or_timeout \
|
|
"${TIMEOUT}" \
|
|
"${CONTAINER_NAME}" \
|
|
/bin/bash -c '[[ $(mailq) == "Mail queue is empty" ]]'
|
|
}
|
|
|
|
|
|
# ? << Functions to wait until a condition is met
|
|
# ! -------------------------------------------------------------------
|
|
# ? >> Miscellaneous helper functions
|
|
|
|
# Adds a mail account and waits for the associated files to be created.
|
|
#
|
|
# @param ${1} = mail account name
|
|
# @param ${2} = password [OPTIONAL]
|
|
# @param ${3} = container name [OPTIONAL]
|
|
function _add_mail_account_then_wait_until_ready() {
|
|
local MAIL_ACCOUNT=${1:?Mail account must be provided}
|
|
local MAIL_PASS="${2:-password_not_relevant_to_test}"
|
|
local CONTAINER_NAME=$(__handle_container_name "${3:-}")
|
|
|
|
# Required to detect a new account and create the maildir:
|
|
_wait_for_service changedetector "${CONTAINER_NAME}"
|
|
|
|
_run_in_container setup email add "${MAIL_ACCOUNT}" "${MAIL_PASS}"
|
|
assert_success
|
|
|
|
_wait_until_account_maildir_exists "${MAIL_ACCOUNT}"
|
|
}
|
|
|
|
# Reloads the postfix service.
|
|
#
|
|
# @param ${1} = container name [OPTIONAL]
|
|
function _reload_postfix() {
|
|
local CONTAINER_NAME=$(__handle_container_name "${1:-}")
|
|
|
|
# Reloading Postfix config after modifying it within 2 seconds will cause Postfix to delay reading `main.cf`:
|
|
# WORKAROUND: https://github.com/docker-mailserver/docker-mailserver/pull/2998
|
|
_exec_in_container touch -d '2 seconds ago' /etc/postfix/main.cf
|
|
_exec_in_container postfix reload
|
|
}
|
|
|
|
# Get the IP of the container (${1}).
|
|
#
|
|
# @param ${1} = container name [OPTIONAL]
|
|
function _get_container_ip() {
|
|
local TARGET_CONTAINER_NAME=$(__handle_container_name "${1:-}")
|
|
docker inspect --format '{{ .NetworkSettings.IPAddress }}' "${TARGET_CONTAINER_NAME}"
|
|
}
|
|
|
|
# Check if a container is running.
|
|
#
|
|
# @param ${1} = container name [OPTIONAL]
|
|
function _container_is_running() {
|
|
local TARGET_CONTAINER_NAME=$(__handle_container_name "${1:-}")
|
|
[[ $(docker inspect -f '{{.State.Running}}' "${TARGET_CONTAINER_NAME}") == 'true' ]]
|
|
}
|
|
|
|
# Checks if the directory exists and then how many files it contains at the top-level.
|
|
#
|
|
# @param ${1} = directory
|
|
# @param ${2} = number of files that should be in ${1}
|
|
function _count_files_in_directory_in_container() {
|
|
local DIRECTORY=${1:?No directory provided}
|
|
local NUMBER_OF_LINES=${2:?No line count provided}
|
|
|
|
_should_have_content_in_directory "${DIRECTORY}" '-type f'
|
|
_should_output_number_of_lines "${NUMBER_OF_LINES}"
|
|
}
|
|
|
|
# Checks if the directory exists and then list the top-level content.
|
|
#
|
|
# @param ${1} = directory
|
|
# @param ${2} = Additional options to `find`
|
|
function _should_have_content_in_directory() {
|
|
local DIRECTORY=${1:?No directory provided}
|
|
local FIND_OPTIONS=${2:-}
|
|
|
|
_run_in_container_bash "[[ -d ${DIRECTORY} ]] && find ${DIRECTORY} -mindepth 1 -maxdepth 1 ${FIND_OPTIONS} -printf '%f\n'"
|
|
assert_success
|
|
}
|
|
|
|
# A simple wrapper for netcat (`nc`). This is useful when sending
|
|
# "raw" e-mails or doing IMAP-related work.
|
|
#
|
|
# @param ${1} = the file that is given to `nc`
|
|
# @param ${1} = custom parameters for `nc` [OPTIONAL] (default: 0.0.0.0 25)
|
|
function _nc_wrapper() {
|
|
local FILE=${1:?Must provide name of template file}
|
|
local NC_PARAMETERS=${2:-0.0.0.0 25}
|
|
|
|
[[ -v CONTAINER_NAME ]] || return 1
|
|
|
|
_run_in_container_bash "nc ${NC_PARAMETERS} < /tmp/docker-mailserver-test/${FILE}"
|
|
}
|
|
|
|
# A simple wrapper for a test that checks whether a file exists.
|
|
#
|
|
# @param ${1} = the path to the file inside the container
|
|
function _file_exists_in_container() {
|
|
_run_in_container_bash "[[ -f ${1} ]]"
|
|
assert_success
|
|
}
|
|
|
|
# A simple wrapper for a test that checks whether a file does not exist.
|
|
#
|
|
# @param ${1} = the path to the file (that should not exists) inside the container
|
|
function _file_does_not_exist_in_container() {
|
|
_run_in_container_bash "[[ -f ${1} ]]"
|
|
assert_failure
|
|
}
|
|
|
|
# ? << Miscellaneous helper functions
|
|
# ! -------------------------------------------------------------------
|