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

@ -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 <EMAIL ADDRESS> <RECIPIENT>
${ORANGE}USAGE${RESET}
./setup.sh alias add <MAIL ALIAS> <RECIPIENT>
${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 "${@}"

View file

@ -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 <USERNAME> [<PASSWORD>]
${ORANGE}USAGE${RESET}
./setup.sh dovecot-master add <MASTER ACCOUNT> [<PASSWORD>]
${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 "${@}"

View file

@ -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 <EMAIL ADDRESS> [<PASSWORD>]
${ORANGE}USAGE${RESET}
./setup.sh email add <MAIL ACCOUNT> [<PASSWORD>]
${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 "${@}"

View file

@ -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 <DOMAIN> <HOST> [<PORT>]
${ORANGE}USAGE${RESET}
./setup.sh relay add-domain <SENDER DOMAIN> <RELAY HOST> [<RELAY PORT>]
${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 "${@}"

View file

@ -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 <domain> <username> <password>" ; }
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 <SENDER DOMAIN> <RELAY ACCOUNT> [<RELAY PASSWORD>]
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 "${@}"

View file

@ -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 <alias@domain> <recipient@other>" ; }
_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 <MAIL ALIAS> <RECIPIENT>
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 "${@}"

View file

@ -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 ] { <MAIL ADDRESS> [<MAIL ADDRESS>${RED}...${RESET}] ${RED}|${RESET} help }
${ORANGE}DESCRIPTION${RESET}
Delete a dovecot master user.
${ORANGE}USAGE${RESET}
./setup.sh dovecot-master del <MASTER ACCOUNT> [<EXTRA MASTER ACCOUNTS> ${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 "${@}"

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 "${@}"

View file

@ -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 <username@domain>' ; }
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 <MAIL ACCOUNT>
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 "${@}"

View file

@ -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 <domain>' ; }
_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 <SENDER 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 "${@}"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 <user@domain> [<quota>]' ; }
_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 <MAIL ACCOUNT> [<QUOTA>]
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 "${@}"

View file

@ -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 <USERNAME> [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 <MASTER ACCOUNT> [<PASSWORD>]
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 "${@}"

View file

@ -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 <user@domain.tld> [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 <MAIL ACCOUNT> [<PASSWORD>]
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 "${@}"

View file

@ -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"
}

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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" {