#!/bin/bash #---------------------------------------------------------------------- # Test suite for InterIMAP # Copyright © 2019 Guilhem Moulin # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . #---------------------------------------------------------------------- set -ue PATH=/usr/bin:/bin export PATH if [ $# -ne 1 ]; then printf "Usage: %s TESTNAME\\n" "$0" >&2 exit 1 fi TEST="${1%/}" TEST="${TEST##*/}" NAME="${TEST#[0-9]*-}" TESTDIR="$(dirname -- "$0")/$TEST" if [ ! -d "$TESTDIR" ]; then printf "ERROR: Not a directory: %s\\n" "$TESTDIR" >&2 exit 1 fi ROOTDIR="$(mktemp --tmpdir="${TMPDIR:-/dev/shm}" --directory "$NAME.XXXXXXXXXX")" trap 'rm -rf -- "$ROOTDIR"' EXIT INT TERM STDOUT="$ROOTDIR/stdout" STDERR="$ROOTDIR/stderr" TMPDIR="$ROOTDIR/tmp" mkdir -- "$TMPDIR" "$ROOTDIR/home" # Set environment for the given user environ_set() { local user="$1" home eval home="\$HOME_$user" ENVIRON=( PATH="$PATH" USER="$user" HOME="$home" XDG_CONFIG_HOME="$home/.config" XDG_DATA_HOME="$home/.local/share" ) } # Prepare the test harness prepare() { declare -a ENVIRON=() local src cfg target u home # copy dovecot config for src in "$TESTDIR/local.conf" "$TESTDIR"/remote*.conf; do [ -r "$src" ] || continue u="${src#"$TESTDIR/"}" u="${u%.conf}" home="$ROOTDIR/home/$u" export "HOME_$u"="$home" mkdir -pm0755 -- "$home/.local/bin" mkdir -pm0700 -- "$home/.config/dovecot" cat >"$home/.config/dovecot/config" <<-EOF log_path = /dev/null mail_home = $ROOTDIR/home/%u mailbox_list_index = yes ssl = no EOF cat >>"$home/.config/dovecot/config" <"$src" environ_set "$u" cat >"$home/.local/bin/doveadm" <<-EOF #!/bin/sh exec env -i ${ENVIRON[@]@Q} \\ doveadm -c ${home@Q}/.config/dovecot/config "\$@" EOF chmod +x -- "$home/.local/bin/doveadm" done # copy interimap config mkdir -pm0700 -- "$HOME_local/.local/share/interimap" mkdir -pm0700 -- "$HOME_local/.config/interimap" for cfg in "$TESTDIR"/remote*.conf; do cfg="${cfg#"$TESTDIR/remote"}" cfg="${cfg%.conf}" u="remote$cfg" eval home="\$HOME_$u" if [ -f "$TESTDIR/interimap.conf" ]; then cat <"$TESTDIR/interimap.conf" >>"$HOME_local/.config/interimap/config$cfg" fi cat >>"$HOME_local/.config/interimap/config$cfg" <<-EOF database = $u.db [local] type = tunnel command = exec ${HOME_local@Q}/.local/bin/doveadm exec imap null-stderr = NO [remote] type = tunnel command = exec ${home@Q}/.local/bin/doveadm exec imap null-stderr = NO EOF done } prepare # Wrappers for interimap(1) and doveadm(1) interimap() { declare -a ENVIRON=() environ_set "local" env -i "${ENVIRON[@]}" perl -I./lib -T ./interimap "$@" } doveadm() { if [ $# -le 2 ] || [ "$1" != "-u" ]; then echo "Usage: doveadm -u USER ..." >&2 exit 1 fi local u="$2" home eval home="\$HOME_$u" shift 2 "$home/.local/bin/doveadm" "$@" } sqlite3() { command sqlite3 -init /dev/null "$@" } # Sample (random) message sample_message() { local date="$(date +"%s.%N")" cat <<-EOF From: To: Date: $(date -R -d@"$date") Message-ID: <$date@example.net> EOF local len="$(shuf -i1-4096 -n1)" xxd -ps -c30 -l"$len" /dev/urandom # 3 to 8329 bytes } # Wrapper for dovecot-lda(1) deliver() { local -a argv while [ $# -gt 0 ] && [ "$1" != "--" ]; do argv+=( "$1" ) shift done if [ $# -gt 0 ] && [ "$1" = "--" ]; then shift fi doveadm "${argv[@]}" exec dovecot-lda -e "$@" } # Dump test results dump_test_result() { local below=">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" local above="<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" local src u home declare -a ENVIRON=() for src in "$TESTDIR/local.conf" "$TESTDIR"/remote*.conf; do u="${src#"$TESTDIR/"}" u="${u%.conf}" environ_set "$u" eval home="\$HOME_$u" printf "%s dovecot configuration:\\n%s\\n" "$u" "$below" env -i "${ENVIRON[@]}" doveconf -c "$home/.config/dovecot/config" -n printf "%s\\n\\n" "$above" done printf "(local) interimap configuration:\\n%s\\n" "$below" cat <"$HOME_local/.config/interimap/config" printf "%s\\n\\n" "$above" printf "standard output was:\\n%s\\n" "$below" cat <"$STDOUT" printf "%s\\n\\n" "$above" printf "standard error was:\\n%s\\n" "$below" cat <"$STDERR" printf "%s\\n\\n" "$above" } # Check mailbox consistency between the local/remote server and interimap's database check_mailbox_status() { local mailbox="$1" lns="inbox" lsep lprefix rns="inbox" rsep rprefix lsep="$(doveconf -c "$HOME_local/.config/dovecot/config" -h "namespace/$lns/separator")" lprefix="$(doveconf -c "$HOME_local/.config/dovecot/config" -h "namespace/$lns/prefix")" rsep="$(doveconf -c "$HOME_remote/.config/dovecot/config" -h "namespace/$lns/separator")" rprefix="$(doveconf -c "$HOME_remote/.config/dovecot/config" -h "namespace/$lns/prefix")" local blob="x'$(printf "%s" "$mailbox" | tr "$lsep" "\\0" | xxd -c256 -ps)'" local rmailbox="$(printf "%s" "$mailbox" | tr "$lsep" "$rsep")" check_mailbox_status2 "$blob" "$lprefix$mailbox" "remote" "$rprefix$rmailbox" } check_mailbox_status2() { local blob="$1" lmailbox="$2" u="$3" rmailbox="$4" local lUIDVALIDITY lUIDNEXT lHIGHESTMODSEQ rUIDVALIDITY rUIDNEXT rHIGHESTMODSEQ read lUIDVALIDITY lUIDNEXT lHIGHESTMODSEQ rUIDVALIDITY rUIDNEXT rHIGHESTMODSEQ < <( sqlite3 "$XDG_DATA_HOME/interimap/$u.db" <<-EOF .mode csv .separator " " "\\n" SELECT l.UIDVALIDITY, l.UIDNEXT, l.HIGHESTMODSEQ, r.UIDVALIDITY, r.UIDNEXT, r.HIGHESTMODSEQ FROM mailboxes m JOIN local l ON m.idx = l.idx JOIN remote r ON m.idx = r.idx WHERE mailbox = $blob EOF ) lHIGHESTMODSEQ="$(printf "%llu" "$lHIGHESTMODSEQ")" rHIGHESTMODSEQ="$(printf "%llu" "$rHIGHESTMODSEQ")" local MESSAGES read MESSAGES < <( sqlite3 "$XDG_DATA_HOME/interimap/$u.db" <<-EOF .mode csv .separator " " "\\n" SELECT COUNT(*) FROM mailboxes a JOIN mapping b ON a.idx = b.idx WHERE mailbox = $blob EOF ) check_mailbox_status_values "local" "$lmailbox" $lUIDVALIDITY $lUIDNEXT $lHIGHESTMODSEQ $MESSAGES check_mailbox_status_values "$u" "$rmailbox" $rUIDVALIDITY $rUIDNEXT $rHIGHESTMODSEQ $MESSAGES local a b a="$(doveadm -u "local" -f "flow" mailbox status "messages unseen vsize" -- "$lmailbox" | \ sed -nr '/.*\s+(\w+=[0-9]+\s+\w+=[0-9]+\s+\w+=[0-9]+)$/ {s//\1/p;q}')" b="$(doveadm -u "$u" -f "flow" mailbox status "messages unseen vsize" -- "$rmailbox" | \ sed -nr '/.*\s+(\w+=[0-9]+\s+\w+=[0-9]+\s+\w+=[0-9]+)$/ {s//\1/p;q}')" if [ "$a" != "$b" ]; then echo "Mailbox $lmailbox status differs: \"$a\" != \"$b\"" >&2 exit 1 fi } check_mailbox_status_values() { local user="$1" mailbox="$2" UIDVALIDITY="$3" UIDNEXT="$4" HIGHESTMODSEQ="$5" MESSAGES="$6" x xs v k xs="$(doveadm -u "$user" -f "flow" mailbox status "uidvalidity uidnext highestmodseq messages" -- "$mailbox" | \ sed -nr '/.*\s+(\w+=[0-9]+\s+\w+=[0-9]+\s+\w+=[0-9]+\s+\w+=[0-9]+)$/ {s//\1/p;q}')" [ -n "$xs" ] || exit 1 for x in $xs; do k="${x%%=*}" case "${k^^[a-z]}" in UIDVALIDITY) v="$UIDVALIDITY";; UIDNEXT) v="$UIDNEXT";; HIGHESTMODSEQ) v="$HIGHESTMODSEQ";; MESSAGES) v="$MESSAGES";; *) echo "Uh? $x" >&2; exit 1 esac if [ "${x#*=}" != "$v" ]; then echo "$user($mailbox): ${k^^[a-z]} doesn't match! ${x#*=} != $v" >&2 exit 1 fi done } check_mailboxes_status() { local mailbox for mailbox in "$@"; do check_mailbox_status "$mailbox" done } # Check mailbox list constency between the local and remote servers check_mailbox_list() { local m i lns="inbox" lsep lprefix rns="inbox" rsep rprefix sub= lsep="$(doveconf -c "$HOME_local/.config/dovecot/config" -h "namespace/$lns/separator")" lprefix="$(doveconf -c "$HOME_local/.config/dovecot/config" -h "namespace/$lns/prefix")" rsep="$(doveconf -c "$HOME_remote/.config/dovecot/config" -h "namespace/$lns/separator")" rprefix="$(doveconf -c "$HOME_remote/.config/dovecot/config" -h "namespace/$lns/prefix")" if [ $# -gt 0 ] && [ "$1" = "-s" ]; then sub="-s" shift fi declare -a lmailboxes=() rmailboxes=() if [ $# -eq 0 ]; then lmailboxes=( "${lprefix}*" ) rmailboxes=( "${rprefix}*" ) else for m in "$@"; do lmailboxes+=( "$lprefix$m" ) rmailboxes+=( "$rprefix${m//"$lsep"/"$rsep"}" ) done fi mapfile -t lmailboxes < <( doveadm -u "local" mailbox list $sub -- "${lmailboxes[@]}" ) for ((i = 0; i < ${#lmailboxes[@]}; i++)); do lmailboxes[i]="${lmailboxes[i]#"$lprefix"}" done mapfile -t rmailboxes < <( doveadm -u "remote" mailbox list $sub -- "${rmailboxes[@]}" ) for ((i = 0; i < ${#rmailboxes[@]}; i++)); do rmailboxes[i]="${rmailboxes[i]#"$rprefix"}" rmailboxes[i]="${rmailboxes[i]//"$rsep"/"$lsep"}" done local IFS=$'\n' diff -u --label="local/mailboxes" --label="remote/mailboxes" \ <( printf "%s" "${lmailboxes[*]}" | sort ) <( printf "%s" "${rmailboxes[*]}" | sort ) } # Wrappers for grep(1) and `grep -C` xgrep() { if ! grep -q "$@"; then printf "\`grep %s\` failed on line %d\\n" "${*@Q}" ${BASH_LINENO[0]} >&2 exit 1 fi } xcgrep() { local m="$1" n shift if ! n="$(grep -c "$@")" || [ $m -ne $n ]; then printf "\`grep -c %s\` failed on line %d: %d != %d\\n" "${*@Q}" ${BASH_LINENO[0]} "$m" "$n" >&2 exit 1 fi } # Run test in a sub-shell declare -a ENVIRON=() environ_set "local" export TMPDIR TESTDIR STDOUT STDERR "${ENVIRON[@]}" export -f environ_set doveadm interimap sqlite3 sample_message deliver export -f check_mailbox_status check_mailbox_status_values check_mailbox_status2 export -f check_mailboxes_status check_mailbox_list xgrep xcgrep printf "%s..." "$TEST" if ! bash -ue "$TESTDIR/run" >"$STDOUT" 2>"$STDERR"; then echo " FAILED" dump_test_result exit 1 else echo " OK" if grep -Paq "\\x00" -- "$STDOUT" "$STDERR"; then printf "\\tWarn: binary output (outstanding \\0)!\\n" fi exit 0 fi