docker-mailserver/target/scripts/helpers/database/db.sh

214 lines
7.2 KiB
Bash

#!/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"
}