#!/bin/bash
#
# Checks whether the templates under config/ still match the files that
# actually run on this server.
#
# The point of it: the templates were derived from a running server. If they
# still agree byte for byte once the placeholders are filled in, that proves a
# fresh installation produces the same setup and that nothing was lost when
# the templates were extracted.
#
# Run on the origin server:
#     install/verify-templates
#
# On a freshly installed server it does the same thing: it shows which files
# have been changed by hand since the installation.

set -uo pipefail

REPO=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)

if [ -f /etc/mailserver/server.conf ]; then
    # shellcheck source=/dev/null
    . /etc/mailserver/server.conf
fi

MAILHOST=${MAILHOST:-$(cat /etc/mailname 2>/dev/null || hostname -f)}
MAILDOMAIN=${MAILDOMAIN:-${MAILHOST#*.}}
SERVER_IP=${SERVER_IP:-$(ip -4 route get 198.51.100.1 2>/dev/null | sed -n 's/.* src \([0-9.]*\).*/\1/p' | head -1)}
SSH_PORT=${SSH_PORT:-$(sshd -T 2>/dev/null | awk '/^port /{print $2; exit}')}
SSH_PORT=${SSH_PORT:-22}

printf 'Comparing against:  %s / %s / %s / ssh %s\n\n' \
    "$MAILHOST" "$MAILDOMAIN" "$SERVER_IP" "$SSH_PORT"

render() {
    sed -e "s|@@MAILHOST@@|$MAILHOST|g" \
        -e "s|@@MAILDOMAIN@@|$MAILDOMAIN|g" \
        -e "s|@@SERVER_IP@@|$SERVER_IP|g" \
        -e "s|@@SSH_PORT@@|$SSH_PORT|g" \
        "$1"
}

# Pairs of template and target. Spelled out deliberately rather than guessed -
# that way a file that is not covered at all stands out.
PAIRS="
config/postfix/main.cf|/etc/postfix/main.cf
config/postfix/master.cf|/etc/postfix/master.cf
config/postfix/submission_header_checks|/etc/postfix/submission_header_checks
config/postfix/postscreen_access.cidr|/etc/postfix/postscreen_access.cidr
config/postfix/sqlite/sender_login.cf|/etc/postfix/sqlite/sender_login.cf
config/postfix/sqlite/virtual_aliases.cf|/etc/postfix/sqlite/virtual_aliases.cf
config/postfix/sqlite/virtual_domains.cf|/etc/postfix/sqlite/virtual_domains.cf
config/postfix/sqlite/virtual_mailboxes.cf|/etc/postfix/sqlite/virtual_mailboxes.cf
config/dovecot/dovecot.conf|/etc/dovecot/dovecot.conf
config/dovecot/sieve/learn-ham.sieve|/etc/dovecot/sieve/learn-ham.sieve
config/dovecot/sieve/learn-spam.sieve|/etc/dovecot/sieve/learn-spam.sieve
config/dovecot/sieve/spam-to-junk.sieve|/etc/dovecot/sieve/spam-to-junk.sieve
config/dovecot/sieve/bin/learn-ham.sh|/etc/dovecot/sieve/bin/learn-ham.sh
config/dovecot/sieve/bin/learn-spam.sh|/etc/dovecot/sieve/bin/learn-spam.sh
config/rspamd/local.d/actions.conf|/etc/rspamd/local.d/actions.conf
config/rspamd/local.d/arc.conf|/etc/rspamd/local.d/arc.conf
config/rspamd/local.d/classifier-bayes.conf|/etc/rspamd/local.d/classifier-bayes.conf
config/rspamd/local.d/dkim_signing.conf|/etc/rspamd/local.d/dkim_signing.conf
config/rspamd/local.d/greylist.conf|/etc/rspamd/local.d/greylist.conf
config/rspamd/local.d/milter_headers.conf|/etc/rspamd/local.d/milter_headers.conf
config/rspamd/local.d/multimap.conf|/etc/rspamd/local.d/multimap.conf
config/rspamd/local.d/options.inc|/etc/rspamd/local.d/options.inc
config/rspamd/local.d/ratelimit.conf|/etc/rspamd/local.d/ratelimit.conf
config/rspamd/local.d/redis.conf|/etc/rspamd/local.d/redis.conf
config/rspamd/local.d/settings.conf|/etc/rspamd/local.d/settings.conf
config/rspamd/local.d/worker-controller.inc|/etc/rspamd/local.d/worker-controller.inc
config/unbound/mailserver.conf|/etc/unbound/unbound.conf.d/mailserver.conf
config/fail2ban/jail.d/mailserver.local|/etc/fail2ban/jail.d/mailserver.local
config/fail2ban/filter.d/dovecot.local|/etc/fail2ban/filter.d/dovecot.local
config/nftables.conf|/etc/nftables.conf
config/mailserver/schema.sql|/etc/mailserver/schema.sql
config/mailserver/README.md|/etc/mailserver/README.md
config/resolv.conf.head|/etc/resolv.conf.head
config/systemd/mailbackup.service|/etc/systemd/system/mailbackup.service
config/systemd/mailbackup.timer|/etc/systemd/system/mailbackup.timer
config/10-reload-mail.sh|/etc/letsencrypt/renewal-hooks/deploy/10-reload-mail.sh
bin/mailbackup|/usr/local/sbin/mailbackup
"

# comments_only answers whether two files differ in comment lines alone. Such
# differences cannot change behaviour and should not flood the display with
# false alarms.
#
# Careful: diff returns 1 when files differ. Together with "set -o pipefail"
# that would fail the whole chain regardless of what grep found - which is why
# the output is collected first and examined afterwards.
comments_only() {
    local differences
    differences=$(diff "$1" "$2" | grep -E '^[<>]' | sed -E 's/^[<>] //' || true)
    ! printf '%s\n' "$differences" | grep -qvE '^[[:space:]]*($|#|--|;)'
}

same=0; differing=0; cosmetic=0; missing=0

while IFS='|' read -r template target; do
    [ -z "$template" ] && continue
    if [ ! -f "$REPO/$template" ]; then
        printf '  ?  %-46s template missing\n' "$template"
        missing=$((missing + 1))
        continue
    fi
    if [ ! -f "$target" ]; then
        printf '  ?  %-46s not installed here\n' "$target"
        missing=$((missing + 1))
        continue
    fi

    rendered=$(mktemp)
    render "$REPO/$template" > "$rendered"

    if cmp -s "$rendered" "$target"; then
        printf '  ok %s\n' "$target"
        same=$((same + 1))
    elif comments_only "$target" "$rendered"; then
        printf '  .. %s  (comments only)\n' "$target"
        cosmetic=$((cosmetic + 1))
    else
        printf '  ~~ %s\n' "$target"
        diff -u "$target" "$rendered" | sed -n '3,$p' | sed 's/^/       /'
        differing=$((differing + 1))
    fi
    rm -f "$rendered"
done <<< "$PAIRS"

printf '\n%d identical, %d differing only in comments, %d really differing, %d missing\n' \
    "$same" "$cosmetic" "$differing" "$missing"

if [ "$differing" != "0" ]; then
    printf '\nA real difference means the running server and the repository have\n'
    printf 'drifted apart. Decide which side is right, then bring the other along.\n'
fi

[ "$differing" = "0" ] && [ "$missing" = "0" ]
