diff --git a/target/bin/addalias b/target/bin/addalias index ad171e46..b5d136ef 100755 --- a/target/bin/addalias +++ b/target/bin/addalias @@ -3,26 +3,37 @@ # shellcheck source=../scripts/helpers/index.sh source /usr/local/bin/helpers/index.sh -DATABASE='/tmp/docker-mailserver/postfix-virtual.cf' +function _main +{ + [[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } + + local MAIL_ALIAS="${1}" + local RECIPIENT="${2}" + + _manage_virtual_aliases_update "${MAIL_ALIAS}" "${RECIPIENT}" \ + || _exit_with_error "'${MAIL_ALIAS}' is already an alias for recipient: '${RECIPIENT}'" +} function __usage { - printf '%s' "${PURPLE}ADDALIAS${RED}(${YELLOW}8${RED}) + printf '%s' "${PURPLE}addalias${RED}(${YELLOW}8${RED}) -${ORANGE}NAME${RESET} - addalias - add an email alias for an existing user - -${ORANGE}SYNOPSIS${RESET} - ./setup.sh alias add +${ORANGE}USAGE${RESET} + ./setup.sh alias add ${ORANGE}OPTIONS${RESET} ${BLUE}Generic Program Information${RESET} help Print the usage information. +${ORANGE}DESCRIPTION${RESET} + Add an alias for a recipient (a mail account). + + Alias and recipient domains can be different. + The recipient domain can be external (eg: @gmail.com). + ${ORANGE}EXAMPLES${RESET} - ${LWHITE}./setup.sh alias add alias-for-me@domain.tld admin@domain.tld${RESET} - Add the alias alias-for-me@doamin.tld for the existing user - admin@domain.tld. + ${LWHITE}./setup.sh alias add alias@example.com recipient@example.com${RESET} + Add the alias 'alias@example.com' for the mail account 'recipient@example.com'. ${ORANGE}EXIT STATUS${RESET} Exit status is 0 if command was successful. If wrong arguments are provided @@ -31,21 +42,4 @@ ${ORANGE}EXIT STATUS${RESET} " } -[[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } - -EMAIL="${1}" -RECIPIENT="${2}" - -[[ -z ${EMAIL} ]] && { __usage ; _exit_with_error 'No alias specified' ; } -[[ -z ${RECIPIENT} ]] && { __usage ; _exit_with_error 'No recipient specified' ; } - -grep \ - -qi "^$(_escape "${EMAIL}")[a-zA-Z@.\ ]*$(_escape "${RECIPIENT}")" \ - "${DATABASE}" 2>/dev/null && _exit_with_error "Alias \"${EMAIL} ${RECIPIENT}\" already exists" - -if grep -qi "^$(_escape "${EMAIL}")" "${DATABASE}" 2>/dev/null -then - sed -i "/${EMAIL}/s/$/,${RECIPIENT}/" "${DATABASE}" -else - echo "${EMAIL} ${RECIPIENT}" >> "${DATABASE}" -fi +_main "${@}" diff --git a/target/bin/adddovecotmasteruser b/target/bin/adddovecotmasteruser index 2fd31572..0f7d47ce 100755 --- a/target/bin/adddovecotmasteruser +++ b/target/bin/adddovecotmasteruser @@ -1,30 +1,41 @@ #! /bin/bash -# shellcheck disable=SC2094 - # shellcheck source=../scripts/helpers/index.sh source /usr/local/bin/helpers/index.sh -DATABASE=/tmp/docker-mailserver/dovecot-masters.cf +function _main +{ + [[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } + + local MAIL_ACCOUNT="${1}" + shift + local PASSWD="${*}" + + _manage_accounts_dovecotmaster_create "${MAIL_ACCOUNT}" "${PASSWD}" +} function __usage { - printf '%s' "${PURPLE}ADDDOVECOTMASTERUSER${RED}(${YELLOW}8${RED}) + printf '%s' "${PURPLE}adddovecotmasteruser${RED}(${YELLOW}8${RED}) -${ORANGE}NAME${RESET} - addmasteruser - add a dovecot master user (for POP3/IMAP administration) - -${ORANGE}SYNOPSIS${RESET} - ./setup.sh dovecot-master add [] +${ORANGE}USAGE${RESET} + ./setup.sh dovecot-master add [] ${ORANGE}OPTIONS${RESET} ${BLUE}Generic Program Information${RESET} help Print the usage information. +${ORANGE}DESCRIPTION${RESET} + Add a new dovecot-master account (for POP3/IMAP administration). + + To avoid a password being logged in the command history of your shell, + you may omit it, you'll be prompted to input the password instead. + ${ORANGE}EXAMPLES${RESET} - ${LWHITE}./setup.sh dovecot-master add test-user${RESET} - Add the dovecot master account 'test-user'. You will be prompted - to input a password afterwards since no password was supplied. + ${LWHITE}./setup.sh dovecot-master add example-account${RESET} + Create the dovecot-master account 'example-account'. + + You will be prompted to input a password afterwards since no password was supplied. ${ORANGE}EXIT STATUS${RESET} Exit status is 0 if command was successful. If wrong arguments are provided @@ -33,27 +44,4 @@ ${ORANGE}EXIT STATUS${RESET} " } -[[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } - -USERNAME="${1}" -shift -PASSWD="${*}" - -[[ -z ${USERNAME} ]] && { __usage ; _exit_with_error 'No username specified' ; } - -touch "${DATABASE}" -_create_lock # Protect config file with lock to avoid race conditions -if grep -qi "^$(_escape "${USERNAME}")|" "${DATABASE}" -then - _exit_with_error "User '${USERNAME}' already exists" -fi - -if [[ -z ${PASSWD} ]] -then - read -r -s -p "Enter Password: " PASSWD - echo - [[ -z ${PASSWD} ]] && _exit_with_error "Password must not be empty" -fi - -HASH="$(doveadm pw -s SHA512-CRYPT -u "${USERNAME}" -p "${PASSWD}")" -echo "${USERNAME}|${HASH}" >> "${DATABASE}" +_main "${@}" diff --git a/target/bin/addmailuser b/target/bin/addmailuser index 0be3b8a4..3827c68f 100755 --- a/target/bin/addmailuser +++ b/target/bin/addmailuser @@ -1,30 +1,45 @@ #! /bin/bash -# shellcheck disable=SC2094 - # shellcheck source=../scripts/helpers/index.sh source /usr/local/bin/helpers/index.sh -DATABASE='/tmp/docker-mailserver/postfix-accounts.cf' +function _main +{ + [[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } + + local MAIL_ACCOUNT="${1}" + shift + local PASSWD="${*}" + + _manage_accounts_create "${MAIL_ACCOUNT}" "${PASSWD}" + + # Change Detection will be triggered from `postfix-accounts.cf` update, + # block until event processed (actual account creation handled there): + _wait_until_account_maildir_exists "${MAIL_ACCOUNT}" +} function __usage { - printf '%s' "${PURPLE}ADDMAILUSER${RED}(${YELLOW}8${RED}) + printf '%s' "${PURPLE}addmailuser${RED}(${YELLOW}8${RED}) -${ORANGE}NAME${RESET} - addmailuser - add an email address (i.e. a user) - -${ORANGE}SYNOPSIS${RESET} - ./setup.sh email add [] +${ORANGE}USAGE${RESET} + ./setup.sh email add [] ${ORANGE}OPTIONS${RESET} ${BLUE}Generic Program Information${RESET} help Print the usage information. +${ORANGE}DESCRIPTION${RESET} + Add a new mail account (email address). + + To avoid a password being logged in the command history of your shell, + you may omit it, you'll be prompted to input the password instead. + ${ORANGE}EXAMPLES${RESET} - ${LWHITE}./setup.sh email add test@domain.tld${RESET} - Add the email account test@domain.tld. You will be prompted - to input a password afterwards since no password was supplied. + ${LWHITE}./setup.sh email add user@example.com${RESET} + Create the email account 'user@example.com'. + + You will be prompted to input a password afterwards since no password was supplied. ${ORANGE}EXIT STATUS${RESET} Exit status is 0 if command was successful. If wrong arguments are provided @@ -33,42 +48,37 @@ ${ORANGE}EXIT STATUS${RESET} " } -[[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } - -FULL_EMAIL="${1}" -shift -PASSWD="${*}" - -[[ -z ${FULL_EMAIL} ]] && { __usage ; _exit_with_error 'No username specified' ; } -[[ ${FULL_EMAIL} =~ .*\@.* ]] || { __usage ; _exit_with_error 'Username must include the domain' ; } - -touch "${DATABASE}" -_create_lock # Protect config file with lock to avoid race conditions -if grep -qi "^$(_escape "${FULL_EMAIL}")|" "${DATABASE}" -then - _exit_with_error "User '${FULL_EMAIL}' already exists" -fi - -if [[ -z ${PASSWD} ]] -then - read -r -s -p "Enter Password: " PASSWD - echo - [[ -z ${PASSWD} ]] && _exit_with_error "Password must not be empty" -fi - -HASH=$(doveadm pw -s SHA512-CRYPT -u "${FULL_EMAIL}" -p "${PASSWD}") -echo "${FULL_EMAIL}|${HASH}" >> "${DATABASE}" - -USER="${FULL_EMAIL%@*}" -DOMAIN="${FULL_EMAIL#*@}" - +# TODO: Remove this method or at least it's usage in `addmailuser`. If tests are failing, correct the tests. +# +# This method was added to delay command completion until a change detection event had processed the newly added user, +# confirmed once maildir was created. It was a workaround to accomodate the test suite apparently, but otherwise +# prevents batch adding users (each one would have to go through their own change detection event). +# +# Originally introduced in PR 1980 (afterwards two futher PRs deleted, and then reverted that deletion): +# https://github.com/docker-mailserver/docker-mailserver/pull/1980 +# Not much details/discussion in the PR, these are the specific commits: +# - Initial commit: https://github.com/docker-mailserver/docker-mailserver/pull/1980/commits/2ed402a12cedd412abcf577e8079137ea05204fe#diff-92d2047e4a9a7965f6ef2f029dd781e09265b0ce171b5322a76e35b66ab4cbf4R67 +# - Follow-up commit: https://github.com/docker-mailserver/docker-mailserver/pull/1980/commits/27542867b20c617b63bbec6fdcba421b65a44fbb#diff-92d2047e4a9a7965f6ef2f029dd781e09265b0ce171b5322a76e35b66ab4cbf4R67 +# +# Original reasoning for this method (sounds like a network storage I/O issue): # Tests fail if the creation of /var/mail/${DOMAIN}/${USER} doesn't happen fast enough after addmailuser executes (check-for-changes.sh race-condition) # Prevent infinite loop in tests like "checking accounts: user3 should have been added to /tmp/docker-mailserver/postfix-accounts.cf even when that file does not exist" -if [[ -e ${CHKSUM_FILE} ]] -then - while [[ ! -d "/var/mail/${DOMAIN}/${USER}" ]] - do - _log 'info' "Waiting for dovecot to create '/var/mail/${DOMAIN}/${USER}/'" - sleep 1 - done -fi +function _wait_until_account_maildir_exists +{ + local MAIL_ACCOUNT=${1} + + if [[ -f ${CHKSUM_FILE} ]] + then + local USER="${MAIL_ACCOUNT%@*}" + local DOMAIN="${MAIL_ACCOUNT#*@}" + + local MAIL_ACCOUNT_STORAGE_DIR="/var/mail/${DOMAIN}/${USER}" + while [[ ! -d ${MAIL_ACCOUNT_STORAGE_DIR} ]] + do + _log 'info' "Waiting for dovecot to create '${MAIL_ACCOUNT_STORAGE_DIR}'" + sleep 1 + done + fi +} + +_main "${@}" diff --git a/target/bin/addrelayhost b/target/bin/addrelayhost index 3cb15a88..1bd606f5 100755 --- a/target/bin/addrelayhost +++ b/target/bin/addrelayhost @@ -3,22 +3,43 @@ # shellcheck source=../scripts/helpers/index.sh source /usr/local/bin/helpers/index.sh -DATABASE='/tmp/docker-mailserver/postfix-relaymap.cf' +function _main +{ + [[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } + + local DOMAIN="${1}" + local HOST="${2}" + local PORT="${3}" + + _validate_parameters + _add_relayhost +} function __usage { - printf '%s' "${PURPLE}ADDRELAYHOST${RED}(${YELLOW}8${RED}) + printf '%s' "${PURPLE}addrelayhost${RED}(${YELLOW}8${RED}) -${ORANGE}NAME${RESET} - addrelayhost - add an relay host - -${ORANGE}SYNOPSIS${RESET} - ./setup.sh relay add-domain [] +${ORANGE}USAGE${RESET} + ./setup.sh relay add-domain [] ${ORANGE}OPTIONS${RESET} ${BLUE}Generic Program Information${RESET} help Print the usage information. +${ORANGE}DESCRIPTION${RESET} + Add a relay-host where mail sent from mail accounts of the provided + domain will be relayed through to their destination. + + If a port is not provided it will default to 25. + + If the relay-host requires authentication, use the 'setup relay add-auth' + command after adding the relay-host. + +${ORANGE}EXAMPLES${RESET} + ${LWHITE}./setup.sh relay add-domain example.com relay.service.test 587${RESET} + Any mail submitted from your '@example.com' accounts will be sent via + relay using the relay-host service at 'relay.service.test:587'. + ${ORANGE}EXIT STATUS${RESET} Exit status is 0 if command was successful. If wrong arguments are provided or arguments contain errors, the script will exit early with exit status 1. @@ -26,21 +47,22 @@ ${ORANGE}EXIT STATUS${RESET} " } -[[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } +function _validate_parameters +{ + [[ -z ${DOMAIN} ]] && { __usage ; _exit_with_error 'No domain specified' ; } + [[ -z ${HOST} ]] && { __usage ; _exit_with_error 'No relay host specified' ; } + [[ -z ${PORT} ]] && PORT=25 +} -DOMAIN="${1}" -HOST="${2}" -PORT="${3}" +# Config is for sender dependent relay-host mapping, +# current support restricts senders to domain scope (port is also enforced). +function _add_relayhost +{ + local SENDER="@${DOMAIN}" + local RELAY_HOST_ENTRY="[${HOST}]:${PORT}" + local DATABASE_RELAY='/tmp/docker-mailserver/postfix-relaymap.cf' -[[ -z ${DOMAIN} ]] && { __usage ; _exit_with_error 'No domain specified' ; } -[[ -z ${HOST} ]] && { __usage ; _exit_with_error 'No relay host specified' ; } -[[ -z ${PORT} ]] && PORT=25 + _db_entry_add_or_replace "${DATABASE_RELAY}" "${SENDER}" "${RELAY_HOST_ENTRY}" +} -if grep -qi "^@${DOMAIN}" "${DATABASE}" 2>/dev/null -then - sed -i \ - "s|^@${DOMAIN}.*|@${DOMAIN}\t\t[${HOST}]:${PORT}|" \ - "${DATABASE}" -else - echo -e "@${DOMAIN}\t\t[${HOST}]:${PORT}" >>"${DATABASE}" -fi +_main "${@}" diff --git a/target/bin/addsaslpassword b/target/bin/addsaslpassword index 8428213d..d625d9d8 100755 --- a/target/bin/addsaslpassword +++ b/target/bin/addsaslpassword @@ -3,31 +3,68 @@ # shellcheck source=../scripts/helpers/index.sh source /usr/local/bin/helpers/index.sh -DATABASE='/tmp/docker-mailserver/postfix-sasl-password.cf' +function _main +{ + [[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } -function __usage { echo "Usage: addsaslpassword " ; } + local DOMAIN="${1}" + local RELAY_ACCOUNT="${2}" + shift 2 + local PASSWD="${*}" -[[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } + _validate_parameters + _add_relayhost_credentials +} -DOMAIN="${1}" -USER="${2}" -PASSWD="${3}" +function __usage +{ + printf '%s' "${PURPLE}addsaslpassword${RED}(${YELLOW}8${RED}) -[[ -z ${DOMAIN} ]] && { __usage ; _exit_with_error 'No domain specified' ; } -[[ -z ${USER} ]] && { __usage ; _exit_with_error 'No username specified' ; } +${ORANGE}USAGE${RESET} + ./setup.sh relay add-auth [] -if [[ -z ${PASSWD} ]] -then - read -r -s -p "Enter Password: " PASSWD - echo - [[ -z ${PASSWD} ]] && _exit_with_error 'Password must not be empty' -fi +${ORANGE}OPTIONS${RESET} + ${BLUE}Generic Program Information${RESET} + help Print the usage information. -if grep -qi "^@${DOMAIN}" "${DATABASE}" 2>/dev/null -then - sed -i \ - "s|^@${DOMAIN}.*|@${DOMAIN}\t\t${USER}:${PASSWD}|" \ - "${DATABASE}" -else - echo -e "@${DOMAIN}\t\t${USER}:${PASSWD}" >>"${DATABASE}" -fi +${ORANGE}DESCRIPTION${RESET} + Add credentials to authenticate to a relay-host service. + + To avoid a password being logged in the command history of your shell, + you may omit it, you'll be prompted to input the password instead. + +${ORANGE}EXAMPLES${RESET} + ${LWHITE}./setup.sh relay add-auth example.com relay-account${RESET} + Any mail submitted for your '@example.com' accounts that is sent + through a relay-host service will authenticate with the credentials: + 'relay-account' + the password you entered at the prompt. + +${ORANGE}EXIT STATUS${RESET} + Exit status is 0 if command was successful. If wrong arguments are provided + or arguments contain errors, the script will exit early with exit status 1. + +" +} + +function _validate_parameters +{ + [[ -z ${DOMAIN} ]] && { __usage ; _exit_with_error 'No domain specified' ; } + [[ -z ${RELAY_ACCOUNT} ]] && { __usage ; _exit_with_error 'No relay account specified' ; } + _password_request_if_missing +} + +# Config is for sender dependent relay-host auth, +# current support restricts senders to their domain scope. +# +# NOTE: This command does not support providing a relay-host +# as the lookup key, it only supports a lookup via sender domain. +function _add_relayhost_credentials +{ + local SENDER="@${DOMAIN}" + local RELAY_HOST_ENTRY_AUTH="${RELAY_ACCOUNT}:${PASSWD}" + local DATABASE_PASSWD='/tmp/docker-mailserver/postfix-sasl-password.cf' + + _db_entry_add_or_replace "${DATABASE_PASSWD}" "${SENDER}" "${RELAY_HOST_ENTRY_AUTH}" +} + +_main "${@}" diff --git a/target/bin/delalias b/target/bin/delalias index a07af1d2..1389b9a4 100755 --- a/target/bin/delalias +++ b/target/bin/delalias @@ -3,21 +3,40 @@ # shellcheck source=../scripts/helpers/index.sh source /usr/local/bin/helpers/index.sh -DATABASE='/tmp/docker-mailserver/postfix-virtual.cf' +function _main +{ + [[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } -EMAIL="${1}" -RECIPIENT="${2}" + local MAIL_ALIAS="${1}" + local RECIPIENT="${2}" -function __usage { echo "Usage: delalias " ; } + _manage_virtual_aliases_delete "${MAIL_ALIAS}" "${RECIPIENT}" +} -[[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } +function __usage +{ + printf '%s' "${PURPLE}delalias${RED}(${YELLOW}8${RED}) -[[ -z ${EMAIL} ]] && { __usage ; _exit_with_error 'No alias specified' ; } -[[ -z ${RECIPIENT} ]] && { __usage ; _exit_with_error 'No recipient specified' ; } -[[ -s ${DATABASE} ]] || exit 0 +${ORANGE}USAGE${RESET} + ./setup.sh alias del -sed -i \ - -e "/^${EMAIL} *${RECIPIENT}$/d" \ - -e "/^${EMAIL}/s/,${RECIPIENT}//g" \ - -e "/^${EMAIL}/s/${RECIPIENT},//g" \ - "${DATABASE}" +${ORANGE}OPTIONS${RESET} + ${BLUE}Generic Program Information${RESET} + help Print the usage information. + +${ORANGE}DESCRIPTION${RESET} + Remove a mail account (the recipient) from an existing alias. + If the alias has no more recipients, the alias will also be removed. + +${ORANGE}EXAMPLES${RESET} + ${LWHITE}./setup.sh alias del alias@example.com recipient@example.com${RESET} + Remove the account 'recipient@example.com' from the alias 'alias@example.com'. + +${ORANGE}EXIT STATUS${RESET} + Exit status is 0 if command was successful. If wrong arguments are provided + or arguments contain errors, the script will exit early with exit status 1. + +" +} + +_main "${@}" diff --git a/target/bin/deldovecotmasteruser b/target/bin/deldovecotmasteruser index b5f20cfd..9e12f716 100755 --- a/target/bin/deldovecotmasteruser +++ b/target/bin/deldovecotmasteruser @@ -1,75 +1,49 @@ #! /bin/bash -# shellcheck disable=SC2094 -# ? This is done to ignore the message "Make sure not to read and write -# ? the same file in the same pipeline", which is a result of ${DATABASE} -# ? being used below. (This disables the message file-wide.) - # shellcheck source=../scripts/helpers/index.sh source /usr/local/bin/helpers/index.sh -DATABASE=/tmp/docker-mailserver/dovecot-masters.cf +function _main +{ + [[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } + + # Validate Parameters: + [[ -z ${*} ]] && { __usage ; _exit_with_error 'No account specified' ; } + + # Actual command to perform: + for MAIL_ACCOUNT in "${@}" + do + _manage_accounts_dovecotmaster_delete "${MAIL_ACCOUNT}" \ + || _exit_with_error "'${MAIL_ACCOUNT}' could not be deleted" + done +} function __usage { - printf '%s' "${PURPLE}DELDOVECOTMASTERUSER${RED}(${YELLOW}8${RED}) + printf '%s' "${PURPLE}deldovecotmasteruser${RED}(${YELLOW}8${RED}) -${ORANGE}NAME${RESET} - deldovecotmasteruser - delete a dovecot master user - -${ORANGE}SYNOPSIS${RESET} - ./setup.sh dovecot-master del [ OPTIONS ] { [${RED}...${RESET}] ${RED}|${RESET} help } - -${ORANGE}DESCRIPTION${RESET} - Delete a dovecot master user. +${ORANGE}USAGE${RESET} + ./setup.sh dovecot-master del [ ${RED}...${RESET} ] ${ORANGE}OPTIONS${RESET} - -h - Show this help dialogue. + ${BLUE}Generic Program Information${RESET} + help Print the usage information. + +${ORANGE}DESCRIPTION${RESET} + Delete a dovecot-master account. ${ORANGE}EXAMPLES${RESET} - ${LWHITE}./setup.sh dovecot-master del administrator${RESET} - Delete the dovecot master user called 'administrator'. + ${LWHITE}./setup.sh dovecot-master del admin${RESET} + Delete the dovecot-master account 'admin'. - ${LWHITE}./setup.sh dovecot-master del administrator admin${RESET} - Delete dovecot master users 'administrator' and 'admin'. + ${LWHITE}./setup.sh dovecot-master del admin extra-admin${RESET} + Delete the two dovecot-master accounts requested. ${ORANGE}EXIT STATUS${RESET} - Exit status is 0 if command was successful, and 1 if there was an error. + Exit status is 0 if command was successful. If wrong arguments are provided + or arguments contain errors, the script will exit early with exit status 1. + " } -if [[ ${1} == 'help' ]] -then - __usage - exit 0 -fi - -shift $((OPTIND-1)) - -[[ -z ${*} ]] && { __usage ; _exit_with_error 'No user specified' ; } -[[ -s ${DATABASE} ]] || exit 0 - -_create_lock # Protect config file with lock to avoid race conditions - -for USER in "${@}" -do - ERROR=false - - # ${USER} must not contain /s and other syntactic characters - UNESCAPED_USER="${USER}" - USER=$(_escape "${USER}") - - if [[ -f ${DATABASE} ]] - then - if ! sedfile --strict -i "/^${USER}|/d" "${DATABASE}" - then - _log 'error' "'${UNESCAPED_USER}' couldn't be deleted in '${DATABASE}'" - ERROR=true - fi - fi - - ${ERROR} && _exit_with_error 'See the messages above.' -done - -exit 0 +_main "${@}" diff --git a/target/bin/delmailuser b/target/bin/delmailuser index bf929d7a..7424e113 100755 --- a/target/bin/delmailuser +++ b/target/bin/delmailuser @@ -1,161 +1,130 @@ #! /bin/bash -# shellcheck disable=SC2094 -# ? This is done to ignore the message "Make sure not to read and write -# ? the same file in the same pipeline", which is a result of ${DATABASE} -# ? being used below. (This disables the message file-wide.) - # shellcheck source=../scripts/helpers/index.sh source /usr/local/bin/helpers/index.sh -DATABASE='/tmp/docker-mailserver/postfix-accounts.cf' -ALIAS_DATABASE='/tmp/docker-mailserver/postfix-virtual.cf' -QUOTA_DATABASE='/tmp/docker-mailserver/dovecot-quotas.cf' -MAILDEL='false' +function _main +{ + [[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } + # Tests expect early exit without error if no DB exists: + [[ -s ${DATABASE_ACCOUNTS} ]] || return 0 + + # Validate Parameters: + [[ -z ${*} ]] && { __usage ; _exit_with_error 'No account specified' ; } + _maildel_request_if_missing + + # TODO: May want to lock all database files prior to loop? (DATABASE_ACCOUNTS DATABASE_QUOTA DATABASE_VIRTUAL) + # NOTE: Present lock method locks the original sourcing script itself. + _create_lock + + # Actual command to perform: + for MAIL_ACCOUNT in "${@}" + do + _account_should_already_exist + + [[ ${MAILDEL} -eq 1 ]] && _remove_maildir "${MAIL_ACCOUNT}" + + _manage_virtual_aliases_delete '_' "${MAIL_ACCOUNT}" \ + || _exit_with_error "Aliases for '${MAIL_ACCOUNT}' could not be deleted" + + _manage_dovecot_quota_delete "${MAIL_ACCOUNT}" \ + || _exit_with_error "Quota for '${MAIL_ACCOUNT}' could not be deleted" + + # Performed last, avoids breaking command if a prior failure occurred + _manage_accounts_delete "${MAIL_ACCOUNT}" \ + || _exit_with_error "'${MAIL_ACCOUNT}' could not be deleted" + + _log 'info' "'${MAIL_ACCOUNT}' and associated data deleted" + done +} function __usage { - printf '%s' "${PURPLE}DELMAILUSER${RED}(${YELLOW}8${RED}) + printf '%s' "${PURPLE}delmailuser${RED}(${YELLOW}8${RED}) -${ORANGE}NAME${RESET} - delmailuser - delete a user and related data - -${ORANGE}SYNOPSIS${RESET} - ./setup.sh email del [ OPTIONS ] { [${RED}...${RESET}] ${RED}|${RESET} help } - -${ORANGE}DESCRIPTION${RESET} - Delete a mail user, aliases, quotas and mail data. +${ORANGE}USAGE${RESET} + ./setup.sh email del [ OPTIONS ] [ ${RED}...${RESET} ] ${ORANGE}OPTIONS${RESET} -y - Indicate that ${LWHITE}all mail data${RESET} is to be deleted without another prompt. + Skip prompt by approving to ${LWHITE}delete all mail storage${RESET} for the account(s). - -h - Show this help dialogue. + ${BLUE}Generic Program Information${RESET} + help Print the usage information. + +${ORANGE}DESCRIPTION${RESET} + Delete a mail account, including associated data (aliases, quotas) and + optionally the mailbox storage for that account. ${ORANGE}EXAMPLES${RESET} - ${LWHITE}./setup.sh email del woohoo@some-domain.org${RESET} - Delete the mail user, quotas and aliases, but ask - again whether mailbox data should be deleted. + ${LWHITE}./setup.sh email del user@example.com${RESET} + Delete the mail account 'user@example.com' and associated data, + but ask if mailbox data should also be deleted. - ${LWHITE}./setup.sh email del -y test@domain.com test@domain.com${RESET} - Delete all mail data for the users 'test' and do not - prompt to ask if all mail data should be deleted. + ${LWHITE}./setup.sh email del -y user@example.com extra-user@example.com${RESET} + Delete the two mail accounts requested, their associated data and + delete the mailbox data for both accounts without asking. ${ORANGE}EXIT STATUS${RESET} - Exit status is 0 if command was successful, and 1 if there was an error. + Exit status is 0 if command was successful. If wrong arguments are provided + or arguments contain errors, the script will exit early with exit status 1. + " } -if [[ ${1} == 'help' ]] -then - __usage - exit 0 -fi +function _parse_options +{ + while getopts ":yY" OPT + do + case "${OPT}" in + ( 'y' | 'Y' ) + MAILDEL=1 + ;; -while getopts ":yYh" OPT -do - case "${OPT}" in - ( 'y' | 'Y' ) - MAILDEL=true - ;; + ( * ) + __usage + _exit_with_error "The option '${OPT}' is unknown" + ;; - ( 'h' ) - __usage - exit 0 - ;; + esac + done +} - ( * ) - __usage - _exit_with_error "The option '${OPT}' is unknown" - ;; +function _maildel_request_if_missing +{ + if [[ ${MAILDEL} -eq 0 ]] + then + local MAILDEL_CHOSEN + read -r -p "Do you want to delete the mailbox as well (removing all mails)? [Y/n] " MAILDEL_CHOSEN - esac -done + # TODO: Why would MAILDEL be set to true if MAILDEL_CHOSEN is empty? + if [[ ${MAILDEL_CHOSEN} =~ (y|Y|yes|Yes) ]] || [[ -z ${MAILDEL_CHOSEN} ]] + then + MAILDEL=1 + fi + fi +} + +function _remove_maildir +{ + local MAIL_ACCOUNT=${1} + + local LOCAL_PART="${MAIL_ACCOUNT%@*}" + local DOMAIN_PART="${MAIL_ACCOUNT#*@}" + local MAIL_ACCOUNT_STORAGE_DIR="/var/mail/${DOMAIN_PART}/${LOCAL_PART}" + + [[ ! -d ${MAIL_ACCOUNT_STORAGE_DIR} ]] && _exit_with_error "Mailbox directory '${MAIL_ACCOUNT_STORAGE_DIR}' does not exist" + + _log 'info' "Deleting Mailbox: '${MAIL_ACCOUNT_STORAGE_DIR}'" + rm -R "${MAIL_ACCOUNT_STORAGE_DIR}" || _exit_with_error 'Mailbox could not be deleted' + # Remove parent directory too if it's empty: + rmdir "/var/mail/${DOMAIN_PART}" &>/dev/null +} + +# Support for optional maildir removal: +MAILDEL=0 +_parse_options "${@}" +# Remove options before passing over parameters to _main: shift $((OPTIND-1)) -[[ -z ${*} ]] && { __usage ; _exit_with_error 'No user specified' ; } -[[ -s ${DATABASE} ]] || exit 0 - -if ! ${MAILDEL} -then - read -r -p "Do you want to delete the mailbox as well (removing all mails)? [Y/n] " MAILDEL_CHOSEN - if [[ ${MAILDEL_CHOSEN} =~ (y|Y|yes|Yes) ]] || [[ -z ${MAILDEL_CHOSEN} ]] - then - MAILDEL=true - fi -fi - -_create_lock # Protect config file with lock to avoid race conditions - -for EMAIL in "${@}" -do - ERROR=false - - USER="${EMAIL%@*}" - DOMAIN="${EMAIL#*@}" - - # ${EMAIL} must not contain /s and other syntactic characters - UNESCAPED_EMAIL="${EMAIL}" - EMAIL=$(_escape "${EMAIL}") - - if [[ -f ${DATABASE} ]] - then - if ! sedfile --strict -i "/^${EMAIL}|/d" "${DATABASE}" - then - _log 'error' "'${UNESCAPED_EMAIL}' couldn't be deleted in '${DATABASE}'" - ERROR=true - fi - fi - - if [[ -f ${ALIAS_DATABASE} ]] - then - # delete all aliases where the user is the only recipient( " ${EMAIL}" ) - # delete user only for all aliases that deliver to multiple recipients ( ",${EMAIL}" "${EMAIL,}" ) - if sed -i \ - -e "/ ${EMAIL}$/d" -e "s/,${EMAIL}//g" -e "s/${EMAIL},//g" \ - "${ALIAS_DATABASE}" - then - _log 'info' "'${UNESCAPED_EMAIL}' and potential aliases deleted" - else - _log 'error' "Aliases for '${UNESCAPED_EMAIL}' couldn't be deleted in '${ALIAS_DATABASE}'" - ERROR=true - fi - fi - - # remove quota directives - if [[ -f ${QUOTA_DATABASE} ]] - then - if ! sedfile --strict -i -e "/^${EMAIL}:.*$/d" "${QUOTA_DATABASE}" - then - _log 'warn' "Quota for '${UNESCAPED_EMAIL}' couldn't be deleted in '${QUOTA_DATABASE}'" - fi - fi - - if ! ${MAILDEL} - then - echo "Leaving the mailbox untouched. -If you want to delete it at a later point, -use 'sudo docker exec mailserver rm -R /var/mail/${DOMAIN}/${USER}'" - exit 0 - fi - - if [[ -e "/var/mail/${DOMAIN}/${USER}" ]] - then - if rm -R "/var/mail/${DOMAIN}/${USER}" - then - _log 'info' 'Mailbox deleted' - else - _log 'error' 'Mailbox could not be deleted' - ERROR=true - fi - rmdir "/var/mail/${DOMAIN}" &>/dev/null - else - log 'error' "Mailbox directory '/var/mail/${DOMAIN}/${USER}' did not exist" - ERROR=true - fi - - ${ERROR} && _exit_with_error 'See the messages above.' -done - -exit 0 +_main "${@}" diff --git a/target/bin/delquota b/target/bin/delquota index 1cd555db..47e89f57 100755 --- a/target/bin/delquota +++ b/target/bin/delquota @@ -3,20 +3,44 @@ # shellcheck source=../scripts/helpers/index.sh source /usr/local/bin/helpers/index.sh -DATABASE='/tmp/docker-mailserver/dovecot-quotas.cf' -USER_DATABASE='/tmp/docker-mailserver/postfix-accounts.cf' +function _main +{ + [[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } -function __usage { echo 'Usage: delquota ' ; } + local MAIL_ACCOUNT="${1}" -[[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } + _validate_parameters + _manage_dovecot_quota_delete "${MAIL_ACCOUNT}" +} -USER="${1}" +function __usage +{ + printf '%s' "${PURPLE}delquota${RED}(${YELLOW}8${RED}) -[[ -z ${USER} ]] && { __usage ; _exit_with_error 'No username specified' ; } -[[ ${USER} =~ .*\@.* ]] || { __usage ; _exit_with_error 'Username must include the domain' ; } +${ORANGE}USAGE${RESET} + ./setup.sh quota del -grep -qE "^${USER}\|" "${USER_DATABASE}" || _exit_with_error "User '${USER}' does not exist" +${ORANGE}OPTIONS${RESET} + ${BLUE}Generic Program Information${RESET} + help Print the usage information. -[[ -s ${DATABASE} ]] || exit 0 +${ORANGE}DESCRIPTION${RESET} + Remove any quota set for an existing mail account. -sed -i -e "/^${USER}:.*$/d" "${DATABASE}" +${ORANGE}EXAMPLES${RESET} + ${LWHITE}./setup.sh quota del user@example.com${RESET} + +${ORANGE}EXIT STATUS${RESET} + Exit status is 0 if command was successful. If wrong arguments are provided + or arguments contain errors, the script will exit early with exit status 1. + +" +} + +function _validate_parameters +{ + _arg_expect_mail_account + _account_should_already_exist +} + +_main "${@}" diff --git a/target/bin/excluderelaydomain b/target/bin/excluderelaydomain index bb5b6096..e4091dcf 100755 --- a/target/bin/excluderelaydomain +++ b/target/bin/excluderelaydomain @@ -3,17 +3,60 @@ # shellcheck source=../scripts/helpers/index.sh source /usr/local/bin/helpers/index.sh -DATABASE='/tmp/docker-mailserver/postfix-relaymap.cf' +function _main +{ + [[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } -DOMAIN="${1}" + local DOMAIN="${1}" + [[ -z ${DOMAIN} ]] && { __usage ; _exit_with_error 'No domain specified' ; } -function __usage { echo 'Usage: excluderelayhost ' ; } + _exclude_domain_from_relayhosts +} -[[ -z ${DOMAIN} ]] && { __usage ; _exit_with_error "no domain specified" ; } +function __usage +{ + printf '%s' "${PURPLE}excluderelayhost${RED}(${YELLOW}8${RED}) -if grep -qi "^@${DOMAIN}" "${DATABASE}" 2>/dev/null -then - sed -i "s/^@${DOMAIN}.*/@${DOMAIN}/" "${DATABASE}" -else - echo -e "@${DOMAIN}" >> "${DATABASE}" -fi +${ORANGE}USAGE${RESET} + ./setup.sh relay exclude-domain + +${ORANGE}OPTIONS${RESET} + ${BLUE}Generic Program Information${RESET} + help Print the usage information. + +${ORANGE}DESCRIPTION${RESET} + When a default relay-host is configured (via ENV), the default behaviour + is to relay all your mail accounts outgoing mail through that service. + + This command allows to opt-out from that default behaviour by excluding + all mail accounts belonging to a hosted domain you specify. + +${ORANGE}EXAMPLES${RESET} + ${LWHITE}./setup.sh relay exclude-domain example.com${RESET} + Any mail submitted from your '@example.com' accounts will be sent + without relaying through a default relay-host (if one was configured). + +${ORANGE}EXIT STATUS${RESET} + Exit status is 0 if command was successful. If wrong arguments are provided + or arguments contain errors, the script will exit early with exit status 1. + +" +} + +# Config is for sender dependent relay-host mapping, +# excludes appending a sender from the real generated mapping in `helpers/relay.sh`. +function _exclude_domain_from_relayhosts +{ + local SENDER="@${DOMAIN}" + local DATABASE_RELAY='/tmp/docker-mailserver/postfix-relaymap.cf' + + # NOTE: No third arg is required. + # This won't cause any problems, a 'space' will be added with the key. + # That helps ensure repeat DB edits for the entry match correctly. + # + # `helpers/relay.sh` is also fine with this, and will eventually drop + # the need for this command entirely once that helper is refactored. + _db_entry_add_or_replace "${DATABASE_RELAY}" "${SENDER}" +} + +_main "${@}" diff --git a/target/bin/listalias b/target/bin/listalias index 0eb90b40..f09643c9 100755 --- a/target/bin/listalias +++ b/target/bin/listalias @@ -3,10 +3,21 @@ # shellcheck source=../scripts/helpers/index.sh source /usr/local/bin/helpers/index.sh -DATABASE='/tmp/docker-mailserver/postfix-virtual.cf' +function _main +{ + local DATABASE_VIRTUAL='/tmp/docker-mailserver/postfix-virtual.cf' + _list_entries "${DATABASE_VIRTUAL}" +} -[[ -f ${DATABASE} ]] || _exit_with_error "No 'postfix-virtual.cf' file" -[[ -s ${DATABASE} ]] || _exit_with_error "Empty 'postfix-virtual.cf' - no aliases have been added" +function _list_entries +{ + local DATABASE=${1} + _db_should_exist_with_content "${DATABASE}" -_get_valid_lines_from_file "${DATABASE}" -exit 0 + while read -r LINE + do + echo -e "* ${LINE}\n" + done < <(_get_valid_lines_from_file "${DATABASE}") +} + +_main diff --git a/target/bin/listdovecotmasteruser b/target/bin/listdovecotmasteruser index d7766781..cf61555b 100755 --- a/target/bin/listdovecotmasteruser +++ b/target/bin/listdovecotmasteruser @@ -3,19 +3,24 @@ # shellcheck source=../scripts/helpers/index.sh source /usr/local/bin/helpers/index.sh -# suppress error output, e.g. when listmailuser runs in a fresh container (DMS not running) -# shellcheck source=/dev/null -source /etc/dms-settings 2>/dev/null +function _main +{ + local DATABASE_DOVECOT_MASTERS='/tmp/docker-mailserver/dovecot-masters.cf' + _list_entries "${DATABASE_DOVECOT_MASTERS}" +} -DATABASE='/tmp/docker-mailserver/dovecot-masters.cf' +function _list_entries +{ + local DATABASE=${1} + _db_should_exist_with_content "${DATABASE}" -[[ -f ${DATABASE} ]] || _exit_with_error "No 'dovecot-masters.cf' file" -[[ -s ${DATABASE} ]] || _exit_with_error "Empty 'dovecot-masters.cf' - no dovecot master accounts have been added" + local MASTER_ACCOUNT + while read -r LINE + do + MASTER_ACCOUNT=$(echo "${LINE}" | cut -d'|' -f1) -while read -r LINE -do - USER=$(echo "${LINE}" | cut -d'|' -f1) - echo "* ${USER}" -done < <(_get_valid_lines_from_file "${DATABASE}") + echo -e "* ${MASTER_ACCOUNT}\n" + done < <(_get_valid_lines_from_file "${DATABASE}") +} -exit 0 +_main diff --git a/target/bin/listmailuser b/target/bin/listmailuser index 8dd7a0e4..0607b6cd 100755 --- a/target/bin/listmailuser +++ b/target/bin/listmailuser @@ -3,51 +3,103 @@ # shellcheck source=../scripts/helpers/index.sh source /usr/local/bin/helpers/index.sh -# suppress error output, e.g. when listmailuser runs in a fresh container (DMS not running) +# Workaround to support ENABLE_QUOTAS toggling during tests: # shellcheck source=/dev/null source /etc/dms-settings 2>/dev/null -function dovecot_quota_to_hr +function _main { - if [[ ${1:-} == "-" ]] + local DATABASE_ACCOUNTS='/tmp/docker-mailserver/postfix-accounts.cf' + local DATABASE_VIRTUAL='/tmp/docker-mailserver/postfix-virtual.cf' + + _list_entries "${DATABASE_ACCOUNTS}" +} + +function _list_entries +{ + local DATABASE=${1} + _db_should_exist_with_content "${DATABASE}" + + local ENTRY_TO_DISPLAY + while read -r LINE + do + ENTRY_TO_DISPLAY=$(_format_list_item "${LINE}") + + echo -e "* ${ENTRY_TO_DISPLAY}\n" + done < <(_get_valid_lines_from_file "${DATABASE}") +} + +function _format_list_item +{ + local LINE=${1} + + local MAIL_ACCOUNT + MAIL_ACCOUNT=$(echo "${LINE}" | cut -d'|' -f1) + + local WITH_QUOTA + WITH_QUOTA=$(_quota_show_for "${MAIL_ACCOUNT}") + + local WITH_ALIASES + WITH_ALIASES=$(_alias_list_for_account "${MAIL_ACCOUNT}") + + local ACCOUNT_ENTRY="${MAIL_ACCOUNT}" + [[ -n ${WITH_QUOTA} ]] && ACCOUNT_ENTRY+=" ${WITH_QUOTA}" + [[ -n ${WITH_ALIASES} ]] && ACCOUNT_ENTRY+="\n [ aliases -> ${WITH_ALIASES} ]" + + echo "${ACCOUNT_ENTRY}" +} + +function _quota_show_for +{ + local MAIL_ACCOUNT=${1} + + [[ ${ENABLE_QUOTAS} -ne 1 ]] && return 0 + + local QUOTA_INFO + # Matches a line where the 3rd column is `type='STORAGE'` - returning the next three column values: + IFS=' ' read -r -a QUOTA_INFO <<< "$(doveadm quota get -u "${MAIL_ACCOUNT}" | tail +2 | awk '{ if ($3 == "STORAGE") { print $4" "$5" "$6 } }')" + + local CURRENT_SIZE SIZE_LIMIT PERCENT_USED + # Format the extracted quota storage columns: + CURRENT_SIZE="$(_bytes_to_human_readable_size "${QUOTA_INFO[0]}")" + SIZE_LIMIT="$(_bytes_to_human_readable_size "${QUOTA_INFO[1]}")" + PERCENT_USED="${QUOTA_INFO[2]}%" + + echo "( ${CURRENT_SIZE} / ${SIZE_LIMIT} ) [${PERCENT_USED}]" +} + +function _bytes_to_human_readable_size +{ + # `-` represents a non-applicable value (eg: Like when `SIZE_LIMIT` is not set): + if [[ ${1:-} == '-' ]] then - echo "~" + echo '~' + # Otherwise a value in KibiBytes (1024 bytes == 1k) is expected (Dovecots internal representation): elif [[ ${1:-} =~ ^[0-9]+$ ]] then + # kibibytes to bytes, converted to approproate IEC unit (eg: MiB): echo $(( 1024 * ${1} )) | numfmt --to=iec else - _exit_with_error "Supplied non-number argument '${1:-}' to 'dovecot_quota_to_hr()' in script 'listmailuser'" + _exit_with_error "Supplied non-number argument '${1:-}' to '_bytes_to_human_readable_size()'" fi } -DATABASE='/tmp/docker-mailserver/postfix-accounts.cf' -ALIASES='/tmp/docker-mailserver/postfix-virtual.cf' +# Returns a comma delimited list of aliases associated to a recipient (ideally the recipient is a mail account): +function _alias_list_for_account +{ + local MAIL_ACCOUNT=${1} -[[ -f ${DATABASE} ]] || _exit_with_error "No 'postfix-accounts.cf' file" -[[ -s ${DATABASE} ]] || _exit_with_error "Empty 'postfix-accounts.cf' - no accounts have been added" + # `__db_list_already_contains_value` would be a more reliable check: + function _account_has_an_alias + { + local ANY_ALIAS='\S\+\s' + grep -qis "^${ANY_ALIAS}.*${MAIL_ACCOUNT}" "${DATABASE_VIRTUAL}" + } -while read -r LINE -do - USER=$(echo "${LINE}" | cut -d'|' -f1) - - if [[ ${ENABLE_QUOTAS} -eq 1 ]] + if _account_has_an_alias then - # ${QUOTA[0]} => current size - # ${QUOTA[1]} => configured size limit - # ${QUOTA[2]} => usage in percent - IFS=' ' read -r -a QUOTA <<< "$(doveadm quota get -u "${USER}" | tail +2 | awk '{ if ($3 == "STORAGE") { print $4" "$5" "$6 } }')" - echo "* ${USER} ( $(dovecot_quota_to_hr "${QUOTA[0]}") / $(dovecot_quota_to_hr "${QUOTA[1]}") ) [${QUOTA[2]}%]" - else - echo "* ${USER}" + grep "${MAIL_ACCOUNT}" "${DATABASE_VIRTUAL}" | awk '{print $1;}' | sed ':a;N;$!ba;s/\n/, /g' fi +} - if [[ -f ${ALIASES} ]] && grep -q "${USER}" "${ALIASES}" - then - echo -e " [ aliases -> $(grep "${USER}" "${ALIASES}" | awk '{print $1;}' | sed ':a;N;$!ba;s/\n/, /g')]\n" - else - echo - fi -done < <(_get_valid_lines_from_file "${DATABASE}") - - -exit 0 +_main diff --git a/target/bin/setquota b/target/bin/setquota index beee5bbf..a230b2f5 100755 --- a/target/bin/setquota +++ b/target/bin/setquota @@ -1,43 +1,76 @@ #! /bin/bash -# ? This is done to ignore the message "Make sure not to read and write -# ? the same file in the same pipeline", which is a result of ${DATABASE} -# ? being used below. (This disables the message file-wide.) -# shellcheck disable=SC2094 - # shellcheck source=../scripts/helpers/index.sh source /usr/local/bin/helpers/index.sh -DATABASE='/tmp/docker-mailserver/dovecot-quotas.cf' -USER_DATABASE='/tmp/docker-mailserver/postfix-accounts.cf' +function _main +{ + [[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } -USER="${1}" -shift -QUOTA="${*}" + local MAIL_ACCOUNT="${1}" + shift + local QUOTA="${*}" -function __usage { echo 'Usage: setquota []' ; } + _validate_parameters + _manage_dovecot_quota_update "${MAIL_ACCOUNT}" "${QUOTA}" +} -[[ -z ${USER} ]] && { __usage ; _exit_with_error "No username specified" ; } -[[ ${USER} =~ .*\@.* ]] || { __usage ; _exit_with_error "Username must include the domain" ; } +function __usage +{ + printf '%s' "${PURPLE}setquota${RED}(${YELLOW}8${RED}) -grep -qE "^${USER}\|" "${USER_DATABASE}" || _exit_with_error "User '${USER}' does not exist" +${ORANGE}USAGE${RESET} + ./setup.sh quota set [] -if [[ -z ${QUOTA} ]] -then - read -r -s 'Enter quota (e.g. 10M): ' QUOTA - echo - [[ -z "${QUOTA}" ]] && _exit_with_error 'Quota must not be empty (use 0 for unlimited quota)' -fi +${ORANGE}OPTIONS${RESET} + ${BLUE}Generic Program Information${RESET} + help Print the usage information. -# check quota -if ! grep -qE "^([0-9]+(B|k|M|G|T)|0)\$" <<< "${QUOTA}" -then - __usage - _exit_with_error 'Invalid quota format. e.g. 302M (B (byte), k (kilobyte), M (megabyte), G (gigabyte) or T (terabyte))' -fi +${ORANGE}DESCRIPTION${RESET} + Set a quota (storage limit) for an existing mail account. -_create_lock # Protect config file with lock to avoid race conditions -touch "${DATABASE}" + The quota value is in bytes. You may use a unit suffix for convenience, + such as 10M for 10 MebiBytes (MiB). A value of 0 opts out of enforcing quota. -delquota "${USER}" -echo "${USER}:${QUOTA}" >>"${DATABASE}" +${ORANGE}EXAMPLES${RESET} + ${LWHITE}./setup.sh quota set user@example.com 5G${RESET} + The account 'user@example.com' is restricted to a 5GiB storage limit. + +${ORANGE}EXIT STATUS${RESET} + Exit status is 0 if command was successful. If wrong arguments are provided + or arguments contain errors, the script will exit early with exit status 1. + +" +} + +function _validate_parameters +{ + # MAIL_ACCOUNT + _arg_expect_mail_account + _account_should_already_exist + + # QUOTA + _quota_request_if_missing + _quota_unit_is_valid +} + +function _quota_request_if_missing +{ + if [[ -z ${QUOTA} ]] + then + read -r -p 'Enter quota (e.g. 10M): ' QUOTA + echo + [[ -z "${QUOTA}" ]] && _exit_with_error 'Quota must not be empty (use 0 for unlimited quota)' + fi +} + +function _quota_unit_is_valid +{ + if ! grep -qE "^([0-9]+(B|k|M|G|T)|0)\$" <<< "${QUOTA}" + then + __usage + _exit_with_error 'Invalid quota format. e.g. 302M (B (byte), k (kilobyte), M (megabyte), G (gigabyte) or T (terabyte))' + fi +} + +_main "${@}" diff --git a/target/bin/updatedovecotmasteruser b/target/bin/updatedovecotmasteruser index d228ec2d..b9628184 100755 --- a/target/bin/updatedovecotmasteruser +++ b/target/bin/updatedovecotmasteruser @@ -1,33 +1,45 @@ #! /bin/bash -# ? This is done to ignore the message "Make sure not to read and write -# ? the same file in the same pipeline", which is a result of ${DATABASE} -# ? being used below. (This disables the message file-wide.) -# shellcheck disable=SC2094 - # shellcheck source=../scripts/helpers/index.sh source /usr/local/bin/helpers/index.sh -DATABASE=/tmp/docker-mailserver/dovecot-masters.cf +function _main +{ + [[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } -USER="${1}" -shift -PASSWD="${*}" + local MAIL_ACCOUNT="${1}" + shift + local PASSWD="${*}" -function __usage { echo 'Usage: updatedovecotmasteruser [PASSWORD]' ; } + _manage_accounts_dovecotmaster_update "${MAIL_ACCOUNT}" "${PASSWD}" +} -[[ -z ${USER} ]] && { __usage ; _exit_with_error 'No username specified' ; } +function __usage +{ + printf '%s' "${PURPLE}updatedovecotmasteruser${RED}(${YELLOW}8${RED}) -if [[ -z ${PASSWD} ]] -then - read -r -s -p 'Enter Password: ' PASSWD - echo - [[ -z ${PASSWD} ]] && _exit_with_error 'Password must not be empty' -fi +${ORANGE}USAGE${RESET} + ./setup.sh dovecot-master update [] -HASH="$(doveadm pw -s SHA512-CRYPT -u "${USER}" -p "${PASSWD}")" +${ORANGE}OPTIONS${RESET} + ${BLUE}Generic Program Information${RESET} + help Print the usage information. -touch "${DATABASE}" -_create_lock # Protect config file with lock to avoid race conditions -grep -qi "^$(_escape "${USER}")|" "${DATABASE}" 2>/dev/null || _exit_with_error "Master user \"${USER}\" does not exist" -sed -i "s ^""${USER}""|.* ""${USER}""|""${HASH}"" " "${DATABASE}" +${ORANGE}DESCRIPTION${RESET} + Update the password for a dovecot-master account. + + To avoid a password being logged in the command history of your shell, + you may omit it, you'll be prompted to input the password instead. + +${ORANGE}EXAMPLES${RESET} + ${LWHITE}./setup.sh dovecot-master update example-account${RESET} + You will be prompted to input a password afterwards since no password was supplied. + +${ORANGE}EXIT STATUS${RESET} + Exit status is 0 if command was successful. If wrong arguments are provided + or arguments contain errors, the script will exit early with exit status 1. + +" +} + +_main "${@}" diff --git a/target/bin/updatemailuser b/target/bin/updatemailuser index d43999c4..7a0d4075 100755 --- a/target/bin/updatemailuser +++ b/target/bin/updatemailuser @@ -1,33 +1,45 @@ #! /bin/bash -# ? This is done to ignore the message "Make sure not to read and write -# ? the same file in the same pipeline", which is a result of ${DATABASE} -# ? being used below. (This disables the message file-wide.) -# shellcheck disable=SC2094 - # shellcheck source=../scripts/helpers/index.sh source /usr/local/bin/helpers/index.sh -DATABASE='/tmp/docker-mailserver/postfix-accounts.cf' +function _main +{ + [[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } -USER="${1}" -shift -PASSWD="${*}" + local MAIL_ACCOUNT="${1}" + shift + local PASSWD="${*}" -function __usage { echo 'Usage: updatemailuser [password]' ; } + _manage_accounts_update "${MAIL_ACCOUNT}" "${PASSWD}" +} -[[ -z ${USER} ]] && { __usage ; _exit_with_error 'No username specified' ; } +function __usage +{ + printf '%s' "${PURPLE}updatemailuser${RED}(${YELLOW}8${RED}) -if [[ -z ${PASSWD} ]] -then - read -r -s -p 'Enter Password: ' PASSWD - echo - [[ -z ${PASSWD} ]] && _exit_with_error 'Password must not be empty' -fi +${ORANGE}USAGE${RESET} + ./setup.sh email update [] -HASH=$(doveadm pw -s SHA512-CRYPT -u "${USER}" -p "${PASSWD}") +${ORANGE}OPTIONS${RESET} + ${BLUE}Generic Program Information${RESET} + help Print the usage information. -touch "${DATABASE}" -_create_lock # Protect config file with lock to avoid race conditions -grep -qi "^$(_escape "${USER}")|" "${DATABASE}" 2>/dev/null || _exit_with_error "User \"${USER}\" does not exist" -sed -i "s ^""${USER}""|.* ""${USER}""|""${HASH}"" " "${DATABASE}" +${ORANGE}DESCRIPTION${RESET} + Update the password for a mail account. + + To avoid a password being logged in the command history of your shell, + you may omit it, you'll be prompted to input the password instead. + +${ORANGE}EXAMPLES${RESET} + ${LWHITE}./setup.sh email update user@example.com${RESET} + You will be prompted to input a password afterwards since no password was supplied. + +${ORANGE}EXIT STATUS${RESET} + Exit status is 0 if command was successful. If wrong arguments are provided + or arguments contain errors, the script will exit early with exit status 1. + +" +} + +_main "${@}" diff --git a/target/scripts/helpers/database/db.sh b/target/scripts/helpers/database/db.sh new file mode 100644 index 00000000..990d0d2f --- /dev/null +++ b/target/scripts/helpers/database/db.sh @@ -0,0 +1,213 @@ +#! /bin/bash + +# Matches relative path to this scripts parent directory, +# Must be defined above any function that would source relative to it: +# shellcheck source-path=target/scripts/helpers/database + +DMS_CONFIG='/tmp/docker-mailserver' +# Modifications are supported for the following databases: +# +# Accounts and Aliases (The 'virtual' kind): +DATABASE_ACCOUNTS="${DMS_CONFIG}/postfix-accounts.cf" +DATABASE_DOVECOT_MASTERS="${DMS_CONFIG}/dovecot-masters.cf" +DATABASE_VIRTUAL="${DMS_CONFIG}/postfix-virtual.cf" +# Dovecot Quota support: +DATABASE_QUOTA="${DMS_CONFIG}/dovecot-quotas.cf" +# Relay-Host support: +DATABASE_PASSWD="${DMS_CONFIG}/postfix-sasl-password.cf" +DATABASE_RELAY="${DMS_CONFIG}/postfix-relaymap.cf" + +# Individual scripts with convenience methods to manage operations easier: +function _db_import_scripts +{ + # This var is stripped by shellcheck from source paths below, + # like the shellcheck source-path above, it shouold match this scripts + # parent directory, with the rest of the relative path in the source lines: + local PATH_TO_SCRIPTS='/usr/local/bin/helpers/database' + + source "${PATH_TO_SCRIPTS}/manage/dovecot-quotas.sh" + source "${PATH_TO_SCRIPTS}/manage/postfix-accounts.sh" + source "${PATH_TO_SCRIPTS}/manage/postfix-virtual.sh" +} +_db_import_scripts + +function _db_entry_add_or_append { _db_operation 'append' "${@}" ; } # Only used by addalias +function _db_entry_add_or_replace { _db_operation 'replace' "${@}" ; } +function _db_entry_remove { _db_operation 'remove' "${@}" ; } + +function _db_operation +{ + local DB_ACTION=${1} + local DATABASE=${2} + local KEY=${3} + # Optional arg: + local VALUE=${4} + + # K_DELIMITER provides a match boundary to avoid accidentally matching substrings: + local K_DELIMITER KEY_LOOKUP + K_DELIMITER=$(__db_get_delimiter_for "${DATABASE}") + # Due to usage in regex pattern, KEY needs to be escaped: + KEY_LOOKUP="$(_escape "${KEY}")${K_DELIMITER}" + + # Support for adding or replacing an entire entry (line): + # White-space delimiter should be written into DATABASE as 'space' character: + local V_DELIMITER="${K_DELIMITER}" + [[ ${V_DELIMITER} == '\s' ]] && V_DELIMITER=' ' + local ENTRY="${KEY}${V_DELIMITER}${VALUE}" + + # Support for 'append' + 'remove' operations on value lists: + # NOTE: Presently only required for `postfix-virtual.cf`. + local _VALUE_ + _VALUE_=$(_escape "${VALUE}") + # `postfix-virtual.cf` is using `,` for delimiting a list of recipients: + [[ ${DATABASE} == "${DATABASE_VIRTUAL}" ]] && V_DELIMITER=',' + + # Perform requested operation: + if _db_has_entry_with_key "${KEY}" "${DATABASE}" + then + # Find entry for key and return status code: + case "${DB_ACTION}" in + ( 'append' ) + __db_list_already_contains_value && return 1 + + sedfile --strict -i "/^${KEY_LOOKUP}/s/$/${V_DELIMITER}${VALUE}/" "${DATABASE}" + ;; + + ( 'replace' ) + ENTRY=$(__escape_sed_replacement "${ENTRY}") + sedfile --strict -i "s/^${KEY_LOOKUP}.*/${ENTRY}/" "${DATABASE}" + ;; + + ( 'remove' ) + if [[ -z ${VALUE} ]] + then # Remove entry for KEY: + sedfile --strict -i "/^${KEY_LOOKUP}/d" "${DATABASE}" + else # Remove target VALUE from entry: + __db_list_already_contains_value || return 0 + + # The delimiter between key and first value may differ from + # the delimiter between multiple values (value list): + local LEFT_DELIMITER="\(${K_DELIMITER}\|${V_DELIMITER}\)" + # If an entry for KEY contains an exact match for VALUE: + # - If VALUE is the only value => Remove entry (line) + # - If VALUE is the last value => Remove VALUE + # - Otherwise => Collapse value to LEFT_DELIMITER (\1) + sedfile --strict -i \ + -e "/^${KEY_LOOKUP}\+${_VALUE_}$/d" \ + -e "/^${KEY_LOOKUP}/s/${V_DELIMITER}${_VALUE_}$//g" \ + -e "/^${KEY_LOOKUP}/s/${LEFT_DELIMITER}${_VALUE_}${V_DELIMITER}/\1/g" \ + "${DATABASE}" + fi + ;; + + ( * ) # Should only fail for developer using this API: + _exit_with_error "Unsupported DB operation: '${DB_ACTION}'" + ;; + + esac + else + # Entry for key does not exist, DATABASE may be empty, or DATABASE does not exist + case "${DB_ACTION}" in + # Fallback action 'Add new entry': + ( 'append' | 'replace' ) + [[ ! -d ${DMS_CONFIG} ]] && mkdir -p "${DMS_CONFIG}" + echo "${ENTRY}" >>"${DATABASE}" + ;; + + # Nothing to remove, return success status + ( 'remove' ) + return 0 + ;; + + ( * ) # This should not happen if using convenience wrapper methods: + _exit_with_error "Unsupported DB operation: '${DB_ACTION}'" + ;; + + esac + fi +} + +# Internal method for: _db_operation +function __db_list_already_contains_value +{ + # Avoids accidentally matching a substring (case-insensitive acceptable): + # 1. Extract the current value of the entry (`\1`), + # 2. If a value list, split into separate lines (`\n`+`g`) at V_DELIMITER, + # 3. Check each line for an exact match of the target VALUE + sed -e "s/^${KEY_LOOKUP}\(.*\)/\1/" \ + -e "s/${V_DELIMITER}/\n/g" \ + "${DATABASE}" | grep -qi "^${_VALUE_}$" +} + + +# Internal method for: _db_operation + _db_has_entry_with_key +# References global vars `DATABASE_*`: +function __db_get_delimiter_for +{ + local DATABASE=${1} + + case "${DATABASE}" in + ( "${DATABASE_ACCOUNTS}" | "${DATABASE_DOVECOT_MASTERS}" ) + echo "|" + ;; + + # NOTE: These files support white-space delimiters, we have not + # historically enforced a specific value; as a workaround + # `_db_operation` will convert to ` ` (space) for writing. + ( "${DATABASE_PASSWD}" | "${DATABASE_RELAY}" | "${DATABASE_VIRTUAL}" ) + echo "\s" + ;; + + ( "${DATABASE_QUOTA}" ) + echo ":" + ;; + + ( * ) + _exit_with_error "Unsupported DB '${DATABASE}'" + ;; + + esac +} + +# sed replacement feature needs to be careful of content containing `/` and `&`, +# `\` can escape these (`/` exists in postfix-account.cf base64 encoded pw hash), +# But otherwise care should be taken with `\`, which should be forbidden for input here? +# NOTE: Presently only `.` is escaped with `\` via `_escape`. +function __escape_sed_replacement +{ + # Matches any `/` or `&`, and escapes them with `\` (`\\\1`): + sed 's/\([/&]\)/\\\1/g' <<< "${ENTRY}" +} + +# +# Validation Methods +# + +function _db_has_entry_with_key +{ + local KEY=${1} + local DATABASE=${2} + + # Fail early if the database file exists but has no content: + [[ -s ${DATABASE} ]] || return 1 + + # K_DELIMITER provides a match boundary to avoid accidentally matching substrings: + local K_DELIMITER KEY_LOOKUP + K_DELIMITER=$(__db_get_delimiter_for "${DATABASE}") + # Due to usage in regex pattern, KEY needs to be escaped: + KEY_LOOKUP="$(_escape "${KEY}")${K_DELIMITER}" + + # NOTE: + # --quiet --no-messages, only return a status code of success/failure. + # --ignore-case as we don't want duplicate keys that vary by case. + # --extended-regexp not used, most regex escaping should be forbidden. + grep --quiet --no-messages --ignore-case "^${KEY_LOOKUP}" "${DATABASE}" +} + +function _db_should_exist_with_content +{ + local DATABASE=${1} + + [[ -f ${DATABASE} ]] || _exit_with_error "'${DATABASE}' does not exist" + [[ -s ${DATABASE} ]] || _exit_with_error "'${DATABASE}' is empty, nothing to list" +} diff --git a/target/scripts/helpers/database/manage/dovecot-quotas.sh b/target/scripts/helpers/database/manage/dovecot-quotas.sh new file mode 100644 index 00000000..3d783f50 --- /dev/null +++ b/target/scripts/helpers/database/manage/dovecot-quotas.sh @@ -0,0 +1,32 @@ +#! /bin/bash + +# Manage DB writes for: DATABASE_QUOTA + +# Logic to perform for requested operations handled here: +function _manage_dovecot_quota +{ + local ACTION=${1} + local MAIL_ACCOUNT=${2} + # Only for ACTION 'update': + local QUOTA=${3} + + local DATABASE_QUOTA='/tmp/docker-mailserver/dovecot-quotas.cf' + case "${ACTION}" in + ( 'update' ) + _db_entry_add_or_replace "${DATABASE_QUOTA}" "${MAIL_ACCOUNT}" "${QUOTA}" + ;; + + ( 'delete' ) + _db_entry_remove "${DATABASE_QUOTA}" "${MAIL_ACCOUNT}" + ;; + + ( * ) # This should not happen if using convenience wrapper methods: + _exit_with_error "Unsupported Action: '${ACTION}'" + ;; + + esac +} + +# Convenience wrappers: +function _manage_dovecot_quota_update { _manage_dovecot_quota 'update' "${@}" ; } # setquota +function _manage_dovecot_quota_delete { _manage_dovecot_quota 'delete' "${@}" ; } # delquota, delmailuser diff --git a/target/scripts/helpers/database/manage/postfix-accounts.sh b/target/scripts/helpers/database/manage/postfix-accounts.sh new file mode 100644 index 00000000..37ae1647 --- /dev/null +++ b/target/scripts/helpers/database/manage/postfix-accounts.sh @@ -0,0 +1,100 @@ +#! /bin/bash + +# Manage DB writes for: +# - DATABASE_ACCOUNTS +# - DATABASE_DOVECOT_MASTERS + +# Logic to perform for requested operations handled here: +function _manage_accounts +{ + local ACTION=${1} + local DATABASE=${2} + local MAIL_ACCOUNT=${3} + # Only for ACTION 'create' or 'update': + local PASSWD=${4} + + _arg_expect_mail_account + + case "${ACTION}" in + ( 'create' | 'update' ) + # Fail early before requesting password: + [[ ${ACTION} == 'create' ]] && _account_should_not_exist_yet + [[ ${ACTION} == 'update' ]] && _account_should_already_exist + _password_request_if_missing + + local PASSWD_HASH + PASSWD_HASH=$(doveadm pw -s SHA512-CRYPT -u "${MAIL_ACCOUNT}" -p "${PASSWD}") + # Early failure above ensures correct operation => Add (create) or Replace (update): + _db_entry_add_or_replace "${DATABASE}" "${MAIL_ACCOUNT}" "${PASSWD_HASH}" + ;; + + ( 'delete' ) + _db_entry_remove "${DATABASE}" "${MAIL_ACCOUNT}" + ;; + + ( * ) # This should not happen if using convenience wrapper methods: + _exit_with_error "Unsupported Action: '${ACTION}'" + ;; + + esac +} + +# Convenience wrappers: +DATABASE_ACCOUNTS='/tmp/docker-mailserver/postfix-accounts.cf' +function _manage_accounts_create { _manage_accounts 'create' "${DATABASE_ACCOUNTS}" "${@}" ; } +function _manage_accounts_update { _manage_accounts 'update' "${DATABASE_ACCOUNTS}" "${@}" ; } +function _manage_accounts_delete { _manage_accounts 'delete' "${DATABASE_ACCOUNTS}" "${@}" ; } + +# Dovecot Master account support can leverage the same management logic: +DATABASE_DOVECOT_MASTERS='/tmp/docker-mailserver/dovecot-masters.cf' +function _manage_accounts_dovecotmaster_create { _manage_accounts 'create' "${DATABASE_DOVECOT_MASTERS}" "${@}" ; } +function _manage_accounts_dovecotmaster_update { _manage_accounts 'update' "${DATABASE_DOVECOT_MASTERS}" "${@}" ; } +function _manage_accounts_dovecotmaster_delete { _manage_accounts 'delete' "${DATABASE_DOVECOT_MASTERS}" "${@}" ; } + +# +# Validation Methods +# + +# These validation helpers rely on: +# - Exteral vars to be declared prior to calling them (MAIL_ACCOUNT, PASSWD, DATABASE). +# - Calling external method '__usage' as part of error handling. + +# Also used by setquota, delquota +function _arg_expect_mail_account +{ + [[ -z ${MAIL_ACCOUNT} ]] && { __usage ; _exit_with_error 'No account specified' ; } + + # Dovecot Master accounts are validated (they are not email addresses): + [[ ${DATABASE} == "${DATABASE_DOVECOT_MASTERS}" ]] && return 0 + + # Account has both local and domain parts: + [[ ${MAIL_ACCOUNT} =~ .*\@.* ]] || { __usage ; _exit_with_error "'${MAIL_ACCOUNT}' should include the domain (eg: user@example.com)" ; } +} + +function _account_should_not_exist_yet +{ + __account_already_exists && _exit_with_error "'${MAIL_ACCOUNT}' already exists" +} + +# Also used by delmailuser, setquota, delquota +function _account_should_already_exist +{ + ! __account_already_exists && _exit_with_error "'${MAIL_ACCOUNT}' does not exist" +} + +function __account_already_exists +{ + local DATABASE=${DATABASE:-"${DATABASE_ACCOUNTS}"} + _db_has_entry_with_key "${MAIL_ACCOUNT}" "${DATABASE}" +} + +# Also used by addsaslpassword +function _password_request_if_missing +{ + if [[ -z ${PASSWD} ]] + then + read -r -s -p 'Enter Password: ' PASSWD + echo + [[ -z ${PASSWD} ]] && _exit_with_error 'Password must not be empty' + fi +} diff --git a/target/scripts/helpers/database/manage/postfix-virtual.sh b/target/scripts/helpers/database/manage/postfix-virtual.sh new file mode 100644 index 00000000..caef1bd3 --- /dev/null +++ b/target/scripts/helpers/database/manage/postfix-virtual.sh @@ -0,0 +1,47 @@ +#! /bin/bash + +# Manage DB writes for: DATABASE_VIRTUAL + +# A virtual alias may be any of `user@domain`, `user`, `@domain`. +# Recipients are local (internal services), hosted (managed accounts), remote (third-party MTA), or aliases themselves, +# An alias may redirect mail to one or more recipients. If a recipient is an alias Postfix will recursively resolve it. +# +# WARNING: Support for multiple and recursive recipients may not be well supported by this projects scripts/features. +# One of those features is Dovecot Quota support, which uses a naive workaround for supporting quota checks for inbound +# mail to an alias address. + +# Logic to perform for requested operations handled here: +function _manage_virtual_aliases +{ + local ACTION=${1} + local MAIL_ALIAS=${2} + local RECIPIENT=${3} + + # Validation error handling expects that the caller has defined a '__usage' method: + [[ -z ${MAIL_ALIAS} ]] && { __usage ; _exit_with_error 'No alias specified' ; } + [[ -z ${RECIPIENT} ]] && { __usage ; _exit_with_error 'No recipient specified' ; } + + local DATABASE_VIRTUAL='/tmp/docker-mailserver/postfix-virtual.cf' + case "${ACTION}" in + # Associate RECIPIENT to MAIL_ALIAS: + ( 'update' ) + _db_entry_add_or_append "${DATABASE_VIRTUAL}" "${MAIL_ALIAS}" "${RECIPIENT}" + ;; + + # Removes RECIPIENT from MAIL_ALIAS - or all aliases when MAIL_ALIAS='_': + # NOTE: If a matched alias has no additional recipients, it is also removed. + ( 'delete' ) + [[ ${MAIL_ALIAS} == '_' ]] && MAIL_ALIAS='\S\+' + _db_entry_remove "${DATABASE_VIRTUAL}" "${MAIL_ALIAS}" "${RECIPIENT}" + ;; + + ( * ) # This should not happen if using convenience wrapper methods: + _exit_with_error "Unsupported Action: '${ACTION}'" + ;; + + esac +} + +# Convenience wrappers: +function _manage_virtual_aliases_update { _manage_virtual_aliases 'update' "${@}" ; } # addalias +function _manage_virtual_aliases_delete { _manage_virtual_aliases 'delete' "${@}" ; } # delalias, delmailuser diff --git a/target/scripts/helpers/index.sh b/target/scripts/helpers/index.sh index 41d1b098..1136db16 100644 --- a/target/scripts/helpers/index.sh +++ b/target/scripts/helpers/index.sh @@ -19,6 +19,8 @@ function _import_scripts source "${PATH_TO_SCRIPTS}/relay.sh" source "${PATH_TO_SCRIPTS}/ssl.sh" source "${PATH_TO_SCRIPTS}/utils.sh" + + source "${PATH_TO_SCRIPTS}/database/db.sh" } _import_scripts diff --git a/test/tests.bats b/test/tests.bats index e831b4f6..dcd71aca 100644 --- a/test/tests.bats +++ b/test/tests.bats @@ -599,7 +599,7 @@ EOF @test "checking accounts: user_without_domain creation should be rejected since user@domain format is required" { run docker exec mail /bin/sh -c "addmailuser user_without_domain mypassword" assert_failure - assert_output --partial "Username must include the domain" + assert_output --partial 'should include the domain (eg: user@example.com)' } @test "checking accounts: user3 should have been added to /tmp/docker-mailserver/postfix-accounts.cf" {