From 57aeb6db2a6041e2ed4f243392315ad2b39bddb6 Mon Sep 17 00:00:00 2001 From: Brennan Kinney <5098581+polarathene@users.noreply.github.com> Date: Fri, 29 Jul 2022 12:10:23 +1200 Subject: [PATCH] refactor: CLI commands for database management (#2654) See the associated PR for more detailed commentary on specific changes. ### Commands refactored: - User (**All:** add / list / update / del + _dovecot-master variants_) - Quota (**All:** set / del) - Virtual Alias (**All:** add / list /del) - Relay (**All:** add-relayhost / add-sasl / exclude-domain) ### Overall changes involve: - **Fairly common structure:** - `_main` method at the top provides an overview of logical steps: - After all methods are declared beneath it (_and imported from the new `helpers/database/db.sh`_), the `_main` is called at the bottom of the file. - `delmailuser` additionally processes option support for `-y` prior to calling `_main`. - `__usage` is now consistent with each of these commands, along with the `help` command. - Most logic delegated to new helper scripts. Some duplicate content remains on the basis that it's low-risk to maintenance and avoids less hassle to jump between files to check a single line, usually this is arg validation. - Error handling should be more consistent, along with var names (_no more `USER`/`EMAIL`/`FULL_EMAIL` to refer to the same expected value_). - **Three new management scripts** (in `helpers/database/manage/`) using a common structure for managing changes to their respective "Database" config file. - `postfix-accounts.sh` unified not only add and update commands, but also all the dovecot-master versions, a single password call for all 4 of them, with a 5th consumer of the password prompt from the relay command `addsaslpassword`. - These scripts delegate actual writes to `helpers/database/db.sh` which provides a common API to support the changes made. - This is more verbose/complex vs the current inline operations each command currently has, as it provides generic support instead of slightly different variations being maintained, along with handling some edge cases that existed and would lead to bugs (notably substring matches). - Centralizing changes here seems wiser than scattered about. I've tried to make it easy to grok, hopefully it's not worse than the current situation. - List operations were kept in their respective commands, `db.sh` is only really managing writes. I didn't see a nice way for removing the code duplication for list commands as the duplication was fairly minimal, especially for `listalias` and `listdovecotmasteruser` which were quite simple in their differences in the loop body. - `listmailuser` and `delmailuser` also retain methods exclusive to respective commands, I wasn't sure if there was any benefit to move those, but they were refactored. --- target/bin/addalias | 50 ++-- target/bin/adddovecotmasteruser | 60 ++--- target/bin/addmailuser | 108 ++++---- target/bin/addrelayhost | 66 +++-- target/bin/addsaslpassword | 81 ++++-- target/bin/delalias | 45 +++- target/bin/deldovecotmasteruser | 86 +++---- target/bin/delmailuser | 237 ++++++++---------- target/bin/delquota | 44 +++- target/bin/excluderelaydomain | 63 ++++- target/bin/listalias | 21 +- target/bin/listdovecotmasteruser | 29 ++- target/bin/listmailuser | 114 ++++++--- target/bin/setquota | 93 ++++--- target/bin/updatedovecotmasteruser | 56 +++-- target/bin/updatemailuser | 56 +++-- target/scripts/helpers/database/db.sh | 213 ++++++++++++++++ .../helpers/database/manage/dovecot-quotas.sh | 32 +++ .../database/manage/postfix-accounts.sh | 100 ++++++++ .../database/manage/postfix-virtual.sh | 47 ++++ target/scripts/helpers/index.sh | 2 + test/tests.bats | 2 +- 22 files changed, 1102 insertions(+), 503 deletions(-) create mode 100644 target/scripts/helpers/database/db.sh create mode 100644 target/scripts/helpers/database/manage/dovecot-quotas.sh create mode 100644 target/scripts/helpers/database/manage/postfix-accounts.sh create mode 100644 target/scripts/helpers/database/manage/postfix-virtual.sh 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" {