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.
This commit is contained in:
Brennan Kinney 2022-07-29 12:10:23 +12:00 committed by GitHub
parent 428477a878
commit 57aeb6db2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1102 additions and 503 deletions

View file

@ -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 ] { <MAIL ADDRESS> [<MAIL ADDRESS>${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 ] <MAIL ACCOUNT> [<EXTRA MAIL ACCOUNTS> ${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 "${@}"