#!/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 [ $# -eq 0 ] || [ $# -gt 2 ]; then printf "Usage: %s TESTFILE [TESTNAME]\\n" "$0" >&2 exit 1 fi BASEDIR="$(dirname -- "$0")" TESTDIR="$BASEDIR/$1" TESTNAME="${2-$1}" if [ ! -d "$TESTDIR" ]; then printf "ERROR: Not a directory: %s\\n" "$TESTDIR" >&2 exit 1 fi ROOTDIR="$(mktemp --tmpdir="${TMPDIR:-/dev/shm}" --directory "$1.XXXXXXXXXX")" declare -a DOVECOT_SERVER=() trap cleanup EXIT INT TERM cleanup() { local pid c for c in "${DOVECOT_SERVER[@]}"; do if [ ! -f "$c" ] || ! env -i PATH="/usr/bin:/bin" doveadm -c "$c" stop; then pid="$(< "${c%/*}/run/master.pid")" kill -TERM "$pid" || printf "kill(1) exited with status %d\\n" "$?" >&2 fi done rm -rf -- "$ROOTDIR" } _STDOUT="$ROOTDIR/stdout" _STDERR="$ROOTDIR/stderr" TMPDIR="$ROOTDIR/tmp" STDERR="$(mktemp --tmpdir="$ROOTDIR" "stderr.XXXXXXXXXX")" mkdir -- "$TMPDIR" "$ROOTDIR/home" declare -a REMOTES=() # 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 n proto if [ -f "$TESTDIR/remotes" ] || [ -L "$TESTDIR/remotes" ]; then for cfg in $(seq 1 "$(< "$TESTDIR/remotes")"); do REMOTES+=( "remote$cfg" ) done else REMOTES+=( "remote" ) fi # copy dovecot config for u in "local" "${REMOTES[@]}"; do home="$ROOTDIR/$u/home" export "HOME_$u"="$home" environ_set "$u" mkdir -pm0700 -- "$home/.dovecot" cat >"$home/.dovecot/config" <<-EOF log_path = $HOME_local/mail.log mail_home = $home mail_location = dbox:~/inbox:LAYOUT=index mailbox_list_index = yes ssl = no listen = 127.0.0.1, ::1 namespace inbox { inbox = yes } EOF if [ -f "$TESTDIR/$u.conf" ] || [ -L "$TESTDIR/$u.conf" ]; then cat >>"$home/.dovecot/config" <"$TESTDIR/$u.conf" fi cp -aT -- "$BASEDIR/snippets/dovecot" "$home/.dovecot/conf.d" cp -at "$home/.dovecot/conf.d" -- "$BASEDIR/certs/ca.crt" "$BASEDIR/certs"/dovecot.* proto="$(env -i "${ENVIRON[@]}" doveconf -c "$home/.dovecot/config" -h protocols)" if [ -n "$proto" ]; then cat >>"$home/.dovecot/config" <<-EOF # https://wiki.dovecot.org/HowTo/Rootless base_dir = $home/.dovecot/run default_internal_user = $(id -un) default_internal_group = $(id -gn) default_login_user = $(id -un) service anvil { chroot = } service imap-login { chroot = } service stats { chroot = } passdb { args = scheme=PLAIN username_format=%u $home/.dovecot/users driver = passwd-file } userdb { args = username_format=%u $home/.dovecot/users driver = passwd-file } EOF env -i PATH="/usr/bin:/bin" /usr/sbin/dovecot -c "$home/.dovecot/config" DOVECOT_SERVER+=( "$home/.dovecot/config" ) printf "%s:%s:::::\\n" "$u" "$(xxd -l16 -p "$home/.dovecot/users" fi mkdir -pm0755 -- "$home/.local/bin" cat >"$home/.local/bin/doveadm" <<-EOF #!/bin/sh exec env -i ${ENVIRON[@]@Q} \\ doveadm -c ${home@Q}/.dovecot/config "\$@" EOF chmod +x -- "$home/.local/bin/doveadm" done # copy interimap and pullimap configuration mkdir -pm0700 -- "$HOME_local/.local/share/interimap" "$HOME_local/.local/share/pullimap" mkdir -pm0700 -- "$HOME_local/.config/interimap" "$HOME_local/.config/pullimap" echo "deliver-rcpt = local" >>"$HOME_local/.config/pullimap/config" for u in "${REMOTES[@]}"; do n="${u#remote}" eval home="\$HOME_$u" cat >>"$HOME_local/.config/interimap/config$n" <<-EOF database = $u.db #logfile = $HOME_local/interimap$n.log EOF if [ -f "$TESTDIR/interimap$n.conf" ] || [ -L "$TESTDIR/interimap$n.conf" ]; then cat <"$TESTDIR/interimap$n.conf" >>"$HOME_local/.config/interimap/config$n" fi if [ -f "$TESTDIR/pullimap.conf" ] || [ -L "$TESTDIR/pullimap.conf" ]; then cat <"$TESTDIR/pullimap.conf" >>"$HOME_local/.config/pullimap/config" fi cat >>"$HOME_local/.config/interimap/config$n" <<-EOF [local] type = tunnel command = exec ${HOME_local@Q}/.local/bin/doveadm exec imap null-stderr = NO EOF if [ -f "$TESTDIR/interimap$n.local" ] || [ -L "$TESTDIR/interimap$n.local" ]; then cat <"$TESTDIR/interimap$n.local" >>"$HOME_local/.config/interimap/config$n" fi if [ -s "$home/.dovecot/users" ]; then cat <<-EOF username = $u password = $(awk -F: -vu="$u" '$1 == u {print $2}' <"$home/.dovecot/users") EOF else cat <<-EOF type = tunnel command = exec ${home@Q}/.local/bin/doveadm exec imap null-stderr = NO EOF fi >"$HOME_local/.$u.conf" if [ -f "$TESTDIR/interimap$n.remote" ] || [ -L "$TESTDIR/interimap$n.remote" ]; then cat <"$TESTDIR/interimap$n.remote" >>"$HOME_local/.$u.conf" fi { printf "\\n[remote]\\n"; cat <"$HOME_local/.$u.conf"; } >>"$HOME_local/.config/interimap/config$n" { printf "\\n[%s]\\n" "$u"; cat <"$HOME_local/.$u.conf"; } >>"$HOME_local/.config/pullimap/config" done } prepare # Wrappers for interimap(1) and doveadm(1) interimap() { _interimap_cmd "interimap" "$@"; } pullimap() { _interimap_cmd "pullimap" "$@"; } _interimap_cmd() { declare -a ENVIRON=() local script="$1" rv=0 shift environ_set "local" env -i "${ENVIRON[@]}" perl -I./lib -T "./$script" "$@" 2>"$STDERR" || rv=$? cat <"$STDERR" >&2 return $rv } interimap_init() { local u="${1-remote}" local db="$XDG_DATA_HOME/interimap/$u.db" local cfg="config${u#remote}" test \! -e "$db" || error "Database already exists" 1 interimap --config "$cfg" || error "Couldn't initialize interimap" 1 test -f "$db" || error "Database is still missing" 1 grep -Fx "Creating new schema in database file $db" <"$STDERR" || error "DB wasn't created" 1 } 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: <$(xxd -ps -l6 /dev/urandom)@example.net> 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() { declare -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 u in "local" "${REMOTES[@]}"; do environ_set "$u" eval home="\$HOME_$u" printf "%s dovecot configuration:\\n%s\\n" "$u" "$below" env -i "${ENVIRON[@]}" doveconf -c "$home/.dovecot/config" -n printf "%s\\n\\n" "$above" done for u in "${REMOTES[@]}"; do printf "interimap configuration (local <-> $u):\\n%s\\n" "$below" cat <"$HOME_local/.config/interimap/config${u#remote}" printf "%s\\n\\n" "$above" done printf "mail.log:\\n%s\\n" "$below" cat -- "$HOME_local/mail.log" 2>/dev/null || true printf "%s\\n\\n" "$above" printf "standard output:\\n%s\\n" "$below" cat <"$_STDOUT" printf "%s\\n\\n" "$above" printf "standard error:\\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/.dovecot/config" -h "namespace/$lns/separator")" lprefix="$(doveconf -c "$HOME_local/.dovecot/config" -h "namespace/$lns/prefix")" rsep="$(doveconf -c "$HOME_remote/.dovecot/config" -h "namespace/$lns/separator")" rprefix="$(doveconf -c "$HOME_remote/.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/.dovecot/config" -h "namespace/$lns/separator")" lprefix="$(doveconf -c "$HOME_local/.dovecot/config" -h "namespace/$lns/prefix")" rsep="$(doveconf -c "$HOME_remote/.dovecot/config" -h "namespace/$lns/separator")" rprefix="$(doveconf -c "$HOME_remote/.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 ) } # Wrapper for `grep -c` xcgrep() { local m="$1" n shift if ! n="$(grep -c "$@")" || [ $m -ne $n ]; then error "\`grep -c ${*@Q}\` failed ($m != $n)" 1 fi } error() { local err="${1+": $1"}" i=${2-0} printf "ERROR$err on file %s line %d\\n" "${BASH_SOURCE[i+1]}" ${BASH_LINENO[i]} >&2 exit 1 } ptree_abort() { local pid for pid in "$@"; do # kill a process and its children pkill -TERM -P "$pid" || printf "pkill(1) exited with status %d\\n" "$?" >&2 kill -TERM "$pid" || printf "kill(1) exited with status %d\\n" "$?" >&2 done wait } step_start() { printf "%s%s..." "${INDENT-}" "$1" >&3; } step_done() { passed >&3; } failed() { [ -t 1 ] && printf " \\x1B[1;31mFAILED\\x1B[0m\\n" || echo " FAILED" } passed() { [ -t 1 ] && printf " \\x1B[1;32mPASSED\\x1B[0m\\n" || echo " PASSED" } # Run test in a sub-shell declare -a ENVIRON=() environ_set "local" export TMPDIR TESTDIR STDERR "${ENVIRON[@]}" export -f environ_set doveadm interimap interimap_init pullimap _interimap_cmd export -f sqlite3 sample_message deliver ptree_abort step_start step_done passed export -f check_mailbox_status check_mailbox_status_values check_mailbox_status2 export -f check_mailboxes_status check_mailbox_list xcgrep error [ "$TESTNAME" = "..." ] || printf "%s%s..." "${INDENT-}" "$TESTNAME" if ! bash -ue "$TESTDIR/t" 3>&1 >"$_STDOUT" 2>"$_STDERR"; then failed [ "${QUIET-n}" = "y" ] || dump_test_result exit 1 else [ "$TESTNAME" = "..." ] || passed if grep -Paq "\\x00" -- "$_STDOUT" "$_STDERR"; then printf "\\tWARN: binary output (outstanding \\0)!\\n" fi exit 0 fi