Scripts

Copy/paste ready helpers for Debian administration

A growing collection of scripts used on Debian servers. These are written for safe operation: strict error handling, predictable output, and correct permissions.

Installing scripts

Quick and safe method

Recommended approach: install into /usr/local/sbin, owned by root, executable by admins.

sudo install -d -m 0750 /usr/local/sbin

sudo install -m 0750 -o root -g root /dev/stdin /usr/local/sbin/SCRIPTNAME.sh <<'EOF'
#!/bin/bash
set -euo pipefail

echo "Replace me"
EOF

sudo hash -r

Tip: keep script files owned by root:root. Avoid world-writable locations.

backup-dirs.sh

Backup directories and files into a single archive

This script creates a timestamped .tar.gz archive and a matching SHA256 checksum file. It supports both directories and individual files, for example:

backup-dirs.sh /etc/dovecot /etc/postfix /etc/nginx
backup-dirs.sh /etc/fstab /etc/aliases /etc/hosts
backup-dirs.sh /etc/postfix/main.cf /etc/postfix/master.cf
#!/bin/bash
set -euo pipefail
umask 077

DESTDIR="/root/bck"
TS="$(date +%F-%H%M%S)"
HOST="$(hostname -s 2>/dev/null || echo host)"
ARCHIVE_BASENAME="backup-${HOST}-${TS}.tar.gz"
ARCHIVE="${DESTDIR}/${ARCHIVE_BASENAME}"
SHAFILE="${ARCHIVE}.sha256"

usage() {
  cat <<'USAGE'
Usage:
  backup-dirs.sh /abs/path1 [/abs/path2 ...]
Examples:
  backup-dirs.sh /etc/dovecot /etc/postfix /etc/nginx
  backup-dirs.sh /etc/dovecot /etc/aliases /etc/fstab
  backup-dirs.sh /etc/postfix/main.cf /etc/postfix/master.cf
USAGE
}

install -d -m 0700 "$DESTDIR"

DEFAULT_PATHS=(
  # "/etc/dovecot"
  # "/etc/postfix"
  # "/etc/aliases"
)

PATHS=()
if (( $# > 0 )); then
  PATHS=("$@")
else
  if (( ${#DEFAULT_PATHS[@]} == 0 )); then
    usage >&2
    echo "ERROR: No paths specified and DEFAULT_PATHS is empty." >&2
    exit 2
  fi
  PATHS=("${DEFAULT_PATHS[@]}")
fi

for p in "${PATHS[@]}"; do
  if [[ "$p" != /* ]]; then
    echo "ERROR: Path must be absolute: $p" >&2
    exit 2
  fi

  if [[ ! -e "$p" && ! -L "$p" ]]; then
    echo "ERROR: Path does not exist: $p" >&2
    exit 2
  fi

  if [[ -b "$p" || -c "$p" || -p "$p" || -S "$p" ]]; then
    echo "ERROR: Refusing special file (block/char/fifo/socket): $p" >&2
    exit 2
  fi
done

REL=()
for p in "${PATHS[@]}"; do
  REL+=("${p#/}")
done

tmp="${ARCHIVE}.tmp"

tar --create \
    --gzip \
    --file "$tmp" \
    --preserve-permissions \
    --acls \
    --xattrs \
    --numeric-owner \
    --warning=no-file-changed \
    -C / \
    "${REL[@]}"

mv -f -- "$tmp" "$ARCHIVE"

tar -tzf "$ARCHIVE" >/dev/null

(
  cd "$DESTDIR"
  sha256sum "$ARCHIVE_BASENAME" > "${ARCHIVE_BASENAME}.sha256"
)

echo "OK: $ARCHIVE"
echo "OK: $SHAFILE"

mailctl

Unified mail administration wrapper

mailctl is the single entrypoint for mail administration actions. It dispatches to helper scripts under /usr/local/sbin and logs actions to /var/log/mailctl.log.

mailctl help
mailctl list

mailctl domain add
mailctl domain delete

mailctl user add
mailctl user delete
mailctl user passwd

mailctl alias add
mailctl alias delete
#!/bin/bash
# mailctl - unified mail administration wrapper

set -euo pipefail
umask 027

PATH="/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/sbin:/usr/local/bin"
export PATH

BASE="/usr/local/sbin"
LOGFILE="/var/log/mailctl.log"

die() {
  echo "ERROR: $*" >&2
  exit 1
}

if [[ "${EUID}" -ne 0 ]]; then
  die "mailctl must be run as root"
fi

if [[ ! -e "$LOGFILE" ]]; then
  install -m 0640 -o root -g adm /dev/null "$LOGFILE"
fi

log() {
  local username="${SUDO_USER:-root}"
  echo "$(date '+%Y-%m-%d %H:%M:%S') [mailctl] user=${username} cmd=\"$*\"" >> "$LOGFILE"
}

main_help() {
  cat <<'EOF'
mailctl - Mail server administration

Usage:
  mailctl <object> <action>
  mailctl <action>

Objects & actions:
  domain add        Add a mail domain
  domain delete     Delete a mail domain (and all users/aliases)

  user add          Add a mailbox
  user delete       Delete a mailbox
  user passwd       Change mailbox password

  alias add         Add a mail alias
  alias delete      Delete a mail alias

Single-word commands:
  list              Show complete mail setup

Help:
  mailctl help
  mailctl help domain
  mailctl help user
  mailctl help alias
EOF
}

domain_help() {
  cat <<'EOF'
mailctl domain

Commands:
  mailctl domain add
  mailctl domain delete

Deleting a domain removes all users and aliases via ON DELETE CASCADE.
EOF
}

user_help() {
  cat <<'EOF'
mailctl user

Commands:
  mailctl user add
  mailctl user delete
  mailctl user passwd
EOF
}

alias_help() {
  cat <<'EOF'
mailctl alias

Commands:
  mailctl alias add
  mailctl alias delete
EOF
}

usage_error() {
  echo "Invalid command" >&2
  echo >&2
  main_help >&2
  exit 1
}

require_script() {
  local f="$1"
  [[ -x "$f" ]] || die "Missing or not executable: $f"
}

dispatch() {
  local script="$1"
  shift || true
  require_script "$script"
  log "$(basename "$script") $*"
  exec "$script" "$@"
}

CMD="${1:-}"
OBJ="${2:-}"

if [[ "$CMD" == "help" || -z "$CMD" ]]; then
  case "$OBJ" in
    "" ) main_help ;;
    domain ) domain_help ;;
    user ) user_help ;;
    alias ) alias_help ;;
    * ) usage_error ;;
  esac
  exit 0
fi

if [[ "$CMD" == "list" ]]; then
  dispatch "$BASE/list-mailsetup.sh"
fi

case "$CMD $OBJ" in
  "domain add")    dispatch "$BASE/add-domain.sh" ;;
  "domain delete") dispatch "$BASE/delete-domain.sh" ;;
  "user add")      dispatch "$BASE/add-mailuser.sh" ;;
  "user delete")   dispatch "$BASE/delete-mailuser.sh" ;;
  "user passwd")   dispatch "$BASE/set-mailpassword.sh" ;;
  "alias add")     dispatch "$BASE/add-alias.sh" ;;
  "alias delete")  dispatch "$BASE/delete-alias.sh" ;;
  *)
    usage_error
    ;;
esac

Mail database helper scripts

Domain / user / alias management

These scripts manage the tables: virtual_domains, virtual_users, and virtual_aliases. They are designed to be used via mailctl.

add-domain.sh

#!/bin/bash
# add-domain.sh - create virtual mail domain

set -euo pipefail

DB="mailserver"
MYSQL="/usr/bin/mysql -N -B"

read -rp "Domain name (e.g. example.nl): " DOMAIN

if ! [[ "$DOMAIN" =~ ^[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
  echo "ERROR: Invalid domain name"
  exit 1
fi

EXISTS=$($MYSQL "$DB" -e \
  "SELECT 1 FROM virtual_domains WHERE name='${DOMAIN}' LIMIT 1;")

if [[ -n "$EXISTS" ]]; then
  echo "ERROR: Domain already exists: $DOMAIN"
  exit 1
fi

$MYSQL "$DB" <<EOF
INSERT INTO virtual_domains (name)
VALUES ('${DOMAIN}');
EOF

echo "OK: Domain created: $DOMAIN"

delete-domain.sh

#!/bin/bash
# delete-domain.sh - remove domain and all related data

set -euo pipefail

DB="mailserver"
MYSQL="/usr/bin/mysql -N -B"

read -rp "Domain to delete: " DOMAIN

if ! [[ "$DOMAIN" =~ ^[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
  echo "ERROR: Invalid domain name"
  exit 1
fi

DOMAIN_ID=$($MYSQL "$DB" -e \
  "SELECT id FROM virtual_domains WHERE name='${DOMAIN}' LIMIT 1;")

if [[ -z "$DOMAIN_ID" ]]; then
  echo "ERROR: Domain not found: $DOMAIN"
  exit 1
fi

echo "WARNING: This will delete:"
echo "  - Domain: $DOMAIN"
echo "  - All users"
echo "  - All aliases"
echo

read -rp "Type the domain name to confirm: " CONFIRM
[[ "$CONFIRM" == "$DOMAIN" ]] || exit 1

$MYSQL "$DB" <<EOF
DELETE FROM virtual_domains WHERE id=${DOMAIN_ID};
EOF

echo "OK: Domain deleted: $DOMAIN"

add-mailuser.sh

#!/bin/bash
# add-mailuser.sh - add virtual mailbox user

set -euo pipefail

DB="mailserver"
MYSQL="/usr/bin/mysql -N -B"

read -rp "Email address: " EMAIL

if ! [[ "$EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
  echo "ERROR: Invalid email address"
  exit 1
fi

DOMAIN="${EMAIL#*@}"

DOMAIN_ID=$($MYSQL "$DB" -e \
  "SELECT id FROM virtual_domains WHERE name='${DOMAIN}' LIMIT 1;")

if [[ -z "$DOMAIN_ID" ]]; then
  echo "ERROR: Domain not found in database: $DOMAIN"
  exit 1
fi

EXISTS=$($MYSQL "$DB" -e \
  "SELECT 1 FROM virtual_users WHERE email='${EMAIL}' LIMIT 1;")

if [[ -n "$EXISTS" ]]; then
  echo "ERROR: User already exists: $EMAIL"
  exit 1
fi

echo "Enter password for $EMAIL"
HASH=$(doveadm pw -s SHA512-CRYPT)

$MYSQL "$DB" <<EOF
INSERT INTO virtual_users (domain_id, email, password)
VALUES (${DOMAIN_ID}, '${EMAIL}', '${HASH}');
EOF

echo "OK: User created: $EMAIL"

echo "Testing authentication (doveadm auth test)"
read -rsp "Re-enter password for test: " TESTPW
echo
doveadm auth test "$EMAIL" "$TESTPW"

echo "OK: Done."

delete-mailuser.sh

#!/bin/bash
# delete-mailuser.sh - remove virtual mail user

set -euo pipefail

DB="mailserver"
MYSQL="/usr/bin/mysql -N -B"

read -rp "Email address to delete: " EMAIL

if ! [[ "$EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
  echo "ERROR: Invalid email address"
  exit 1
fi

EXISTS=$($MYSQL "$DB" -e \
  "SELECT 1 FROM virtual_users WHERE email='${EMAIL}' LIMIT 1;")

if [[ -z "$EXISTS" ]]; then
  echo "ERROR: User not found: $EMAIL"
  exit 1
fi

read -rp "Really delete user $EMAIL? [y/N]: " CONFIRM
[[ "$CONFIRM" =~ ^[Yy]$ ]] || exit 0

$MYSQL "$DB" <<EOF
DELETE FROM virtual_users WHERE email='${EMAIL}';
EOF

echo "OK: User deleted: $EMAIL"

set-mailpassword.sh

#!/bin/bash
# set-mailpassword.sh - change virtual mailbox password

set -euo pipefail

DB="mailserver"
MYSQL="/usr/bin/mysql -N -B"

read -rp "Email address: " EMAIL

if ! [[ "$EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
  echo "ERROR: Invalid email address"
  exit 1
fi

EXISTS=$($MYSQL "$DB" -e \
  "SELECT 1 FROM virtual_users WHERE email='${EMAIL}' LIMIT 1;")

if [[ -z "$EXISTS" ]]; then
  echo "ERROR: User not found: $EMAIL"
  exit 1
fi

echo "Enter new password for $EMAIL"
HASH=$(doveadm pw -s SHA512-CRYPT)

$MYSQL "$DB" <<EOF
UPDATE virtual_users
SET password='${HASH}'
WHERE email='${EMAIL}';
EOF

echo "OK: Password updated for: $EMAIL"

echo "Testing authentication (doveadm auth test)"
read -rsp "Re-enter password for test: " TESTPW
echo
doveadm auth test "$EMAIL" "$TESTPW"

echo "OK: Done."

add-alias.sh

#!/bin/bash
# add-alias.sh - create virtual mail alias

set -euo pipefail

DB="mailserver"
MYSQL="/usr/bin/mysql -N -B"

read -rp "Alias address (source): " SOURCE
read -rp "Destination address: " DEST

EMAIL_RE='^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'

if ! [[ "$SOURCE" =~ $EMAIL_RE ]]; then
  echo "ERROR: Invalid source address"
  exit 1
fi

if ! [[ "$DEST" =~ $EMAIL_RE ]]; then
  echo "ERROR: Invalid destination address"
  exit 1
fi

DOMAIN="${SOURCE#*@}"

DOMAIN_ID=$($MYSQL "$DB" -e \
  "SELECT id FROM virtual_domains WHERE name='${DOMAIN}' LIMIT 1;")

if [[ -z "$DOMAIN_ID" ]]; then
  echo "ERROR: Domain not found: $DOMAIN"
  exit 1
fi

EXISTS=$($MYSQL "$DB" -e \
  "SELECT 1 FROM virtual_aliases WHERE source='${SOURCE}' LIMIT 1;")

if [[ -n "$EXISTS" ]]; then
  echo "ERROR: Alias already exists: $SOURCE"
  exit 1
fi

$MYSQL "$DB" <<EOF
INSERT INTO virtual_aliases (domain_id, source, destination)
VALUES (${DOMAIN_ID}, '${SOURCE}', '${DEST}');
EOF

echo "OK: Alias created: $SOURCE -> $DEST"

delete-alias.sh

#!/bin/bash
# delete-alias.sh - remove virtual alias

set -euo pipefail

DB="mailserver"
MYSQL="/usr/bin/mysql -N -B"

read -rp "Alias address to delete: " SOURCE

if ! [[ "$SOURCE" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
  echo "ERROR: Invalid email address"
  exit 1
fi

EXISTS=$($MYSQL "$DB" -e \
  "SELECT 1 FROM virtual_aliases WHERE source='${SOURCE}' LIMIT 1;")

if [[ -z "$EXISTS" ]]; then
  echo "ERROR: Alias not found: $SOURCE"
  exit 1
fi

$MYSQL "$DB" <<EOF
DELETE FROM virtual_aliases WHERE source='${SOURCE}';
EOF

echo "OK: Alias deleted: $SOURCE"