1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2025-04-04 00:17:06 +00:00
mailinabox/tests/suites/_mail-functions.sh
downtownallday 1f0d2ddb92 Issue #1340 - LDAP backend for accounts
This commit will:

1. Change the user account database from sqlite to OpenLDAP
2. Add policyd-spf to postfix for SPF validation
3. Add a test runner with some automated test suites

Notes:

User account password hashes are preserved.

There is a new Roundcube contact list called "Directory" that lists the users in LDAP (MiaB users), similar to what Google Suite does.

Users can still change their password in Roundcube.

OpenLDAP is configured with TLS, but all remote access is blocked by firewall rules. Manual changes are required to open it for remote access (eg. "ufw allow proto tcp from <HOST> to any port ldaps").

The test runner is started by executing tests/runner.sh. Be aware that it will make changes to your system, including adding new users, domains, mailboxes, start/stop services, etc. It is highly unadvised to run it on a production system!

The LDAP schema that supports mail delivery with postfix and dovecot is located in conf/postfix.schema. This file is copied verbatim from the LdapAdmin project (GPL, ldapadmin.org). Instead of including the file in git, it could be referenced by URL and downloaded by the setup script if GPL is an issue or apply for a PEN from IANA.

Mangement console and other services should not appear or behave any differently than before.
2020-01-17 17:03:21 -05:00

370 lines
9.6 KiB
Bash

# -*- indent-tabs-mode: t; tab-width: 4; -*-
clear_postfix_queue() {
record "[Clear postfix queue]"
postsuper -d ALL >>$TEST_OF 2>&1 || die "Unable to clear postfix undeliverable mail queue"
}
ensure_root_user() {
# ensure there is a local email account for root.
#
# on exit, ROOT, ROOT_MAILDROP, and ROOT_DN are set, and if no
# account exists, a new root@$(hostname) is created having a
# random password
#
if [ ! -z "$ROOT_MAILDROP" ]; then
# already have it
return
fi
ROOT="${USER}@$(hostname)"
record "[Find user $ROOT]"
get_attribute "$LDAP_USERS_BASE" "mail=$ROOT" "maildrop"
ROOT_MAILDROP="$ATTR_VALUE"
ROOT_DN="$ATTR_DN"
if [ -z "$ROOT_DN" ]; then
local pw="$(generate_password 128)"
create_user "$ROOT" "$pw"
record "new password is: $pw"
ROOT_DN="$ATTR_DN"
ROOT_MAILDROP="$ROOT"
else
record "$ROOT => $ROOT_DN ($ROOT_MAILDROP)"
fi
}
dovecot_mailbox_home() {
local email="$1"
echo -n "${STORAGE_ROOT}/mail/mailboxes/"
awk -F@ '{print $2"/"$1}' <<< "$email"
}
start_log_capture() {
SYS_LOG_LINECOUNT=$(wc -l /var/log/syslog 2>>$TEST_OF | awk '{print $1}') || die "could not access /var/log/syslog"
SLAPD_LOG_LINECOUNT=0
if [ -e /var/log/ldap/slapd.log ]; then
SLAPD_LOG_LINECOUNT=$(wc -l /var/log/ldap/slapd.log 2>>$TEST_OF | awk '{print $1}') || die "could not access /var/log/ldap/slapd.log"
fi
MAIL_ERRLOG_LINECOUNT=0
if [ -e /var/log/mail.err ]; then
MAIL_ERRLOG_LINECOUNT=$(wc -l /var/log/mail.err 2>>$TEST_OF | awk '{print $1}') || die "could not access /var/log/mail.err"
fi
MAIL_LOG_LINECOUNT=0
if [ -e /var/log/mail.log ]; then
MAIL_LOG_LINECOUNT=$(wc -l /var/log/mail.log 2>>$TEST_OF | awk '{print $1}') || die "could not access /var/log/mail.log"
fi
DOVECOT_LOG_LINECOUNT=$(doveadm log errors 2>>$TEST_OF | wc -l | awk '{print $1}') || die "could not access doveadm error logs"
}
start_mail_capture() {
local email="$1"
local newdir="$(dovecot_mailbox_home "$email")/new"
record "[Start mail capture $email]"
DOVECOT_CAPTURE_USER="$email"
DOVECOT_CAPTURE_FILECOUNT=0
if [ -e "$newdir" ]; then
DOVECOT_CAPTURE_FILECOUNT=$(ls "$newdir" 2>>$TEST_OF | wc -l)
[ $? -ne 0 ] && die "Error accessing mailbox of $email"
fi
record "mailbox: $(dirname $newdir)"
record "mailbox has $DOVECOT_CAPTURE_FILECOUNT files"
}
dump_capture_logs() {
# dump log files
record "[capture log dump]"
echo ""
echo "============= SYSLOG ================"
tail --lines=+$SYS_LOG_LINECOUNT /var/log/syslog 2>>$TEST_OF
echo ""
echo "============= SLAPD ================="
tail --lines=+$SLAPD_LOG_LINECOUNT /var/log/ldap/slapd.log 2>>$TEST_OF
echo ""
echo "============= MAIL.ERR =============="
tail --lines=+$MAIL_ERRLOG_LINECOUNT /var/log/mail.err 2>>$TEST_OF
echo ""
echo "============= MAIL.LOG =============="
tail --lines=+$MAIL_LOG_LINECOUNT /var/log/mail.log 2>>$TEST_OF
echo ""
echo "============= DOVECOT ERRORS =============="
doveadm log errors | tail --lines=+$DOVECOT_LOG_LINECOUNT 2>>$TEST_OF
}
detect_syslog_error() {
record
record "[Detect syslog errors]"
local count
let count="$SYS_LOG_LINECOUNT + 1"
tail --lines=+$count /var/log/syslog 2>>$TEST_OF | (
let ec=0 # error count
while read line; do
awk '
/status=(bounced|deferred|undeliverable)/ { exit 1 }
!/postfix\/qmgr/ && /warning:/ { exit 1 }
/(fatal|reject|error):/ { exit 1 }
/Error in / { exit 1 }
/named\[\d+\]:.* verify failed/ { exit 1 }
' \
>>$TEST_OF 2>&1 <<< "$line"
if [ $? -eq 1 ]; then
let ec+=1
record "$F_DANGER[ERROR] $line$F_RESET"
else
record "[ OK] $line"
fi
done
[ $ec -gt 0 ] && exit 0
exit 1 # no errors
)
local x=( ${PIPESTATUS[*]} )
[ ${x[0]} -ne 0 ] && die "Could not read /var/log/syslog"
return ${x[1]}
}
detect_slapd_log_error() {
record
record "[Detect slapd log errors]"
local count
let count="SLAPD_LOG_LINECOUNT + 1"
tail --lines=+$count /var/log/ldap/slapd.log 2>>$TEST_OF | (
let ec=0 # error count
let wc=0 # warning count
let ignored=0
while read line; do
# slapd error 68 = "entry already exists". Mark it as a
# warning because code often attempts to add entries
# silently ignoring the error, which is expected behavior
#
# slapd error 32 = "no such object". Mark it as a warning
# because code often attempts to resolve a dn (eg member)
# that is orphaned, so no entry exists. Code may or may
# not care about this.
#
# slapd error 4 - "size limit exceeded". Mark it as a warning
# because code often attempts to find just 1 entry so sets
# the limit to 1 purposefully.
#
# slapd error 20 - "attribute or value exists". Mark it as a
# warning becuase code often attempts to add a new value
# to an existing attribute and doesn't care if the new
# value fails to add because it already exists.
#
awk '
/SEARCH RESULT.*err=(32|4) / { exit 2}
/RESULT.*err=(68|20) / { exit 2 }
/ not indexed/ { exit 2 }
/RESULT.*err=[^0]/ { exit 1 }
/(get|test)_filter/ { exit 3 }
/mdb_(filter|list)_candidates/ { exit 3 }
/:( | #011| )(AND|OR|EQUALITY)/ { exit 3 }
' \
>>$TEST_OF 2>&1 <<< "$line"
r=$?
if [ $r -eq 1 ]; then
let ec+=1
record "$F_DANGER[ERROR] $line$F_RESET"
elif [ $r -eq 2 ]; then
let wc+=1
record "$F_WARN[WARN ] $line$F_RESET"
elif [ $r -eq 3 ]; then
let ignored+=1
else
record "[OK ] $line"
fi
done
record "$ignored unreported/ignored log lines"
[ $ec -gt 0 ] && exit 0
exit 1 # no errors
)
local x=( ${PIPESTATUS[*]} )
[ ${x[0]} -ne 0 ] && die "Could not read /var/log/ldap/slapd.log"
return ${x[1]}
}
detect_dovecot_log_error() {
record
record "[Detect dovecot log errors]"
local count
let count="$MAIL_LOG_LINECOUNT + 1"
if [ ! -e /var/log/mail.log ]; then
return 0
fi
# prefer mail.log over `dovadm log errors` because the latter does
# not have as much output - it's helpful to have success logs when
# diagnosing logs...
cat /var/log/mail.log 2>>$TEST_OF | tail --lines=+$count | (
let ec=0 # error count
let ignored=0
while read line; do
awk '
/LDAP server, reconnecting/ { exit 2 }
/postfix/ { exit 2 }
/auth failed/ { exit 1 }
/ Error: / { exit 1 }
' \
>>$TEST_OF 2>&1 <<< "$line"
r=$?
if [ $r -eq 1 ]; then
let ec+=1
record "$F_DANGER[ERROR] $line$F_RESET"
elif [ $r -eq 2 ]; then
let ignored+=1
else
record "[ OK] $line"
fi
done
record "$ignored unreported/ignored log lines"
[ $ec -gt 0 ] && exit 0
exit 1 # no errors
)
local x=( ${PIPESTATUS[*]} )
[ ${x[0]} -ne 0 -o ${x[1]} -ne 0 ] && die "Could not read mail log"
return ${x[2]}
}
check_logs() {
local assert="${1:-false}"
[ "$1" == "true" -o "$1" == "false" ] && shift
local types=($@)
[ ${#types[@]} -eq 0 ] && types=(syslog slapd mail)
# flush records
kill -HUP $(cat /var/run/rsyslogd.pid)
sleep 2
if array_contains syslog ${types[@]}; then
detect_syslog_error && $assert &&
test_failure "detected errors in syslog"
fi
if array_contains slapd ${types[@]}; then
detect_slapd_log_error && $assert &&
test_failure "detected errors in slapd log"
fi
if array_contains mail ${types[@]}; then
detect_dovecot_log_error && $assert &&
test_failure "detected errors in dovecot log"
fi
}
assert_check_logs() {
check_logs true $@
}
grep_postfix_log() {
local msg="$1"
local count
let count="$SYS_LOG_LINECOUNT + 1"
tail --lines=+$count /var/log/syslog 2>>$TEST_OF | grep -iF "$msg" >/dev/null 2>>$TEST_OF
return $?
}
wait_mail() {
local x mail_files elapsed max_s="${1:-60}"
let elapsed=0
record "[Waiting for mail to $DOVECOT_CAPTURE_USER]"
while [ $elapsed -lt $max_s ]; do
mail_files=( $(get_captured_mail_files) )
[ ${#mail_files[*]} -gt 0 ] && break
sleep 1
let elapsed+=1
let x="$elapsed % 10"
[ $x -eq 0 ] && record "...~${elapsed} seconds has passed"
done
if [ $elapsed -ge $max_s ]; then
record "Timeout waiting for mail"
return 1
fi
record "new mail files:"
for x in ${mail_files[@]}; do
record "$x"
done
}
get_captured_mail_files() {
local newdir="$(dovecot_mailbox_home "$DOVECOT_CAPTURE_USER")/new"
local count
let count="$DOVECOT_CAPTURE_FILECOUNT + 1"
[ ! -e "$newdir" ] && return 0
# output absolute path names
local file
for file in $(ls "$newdir" 2>>$TEST_OF | tail --lines=+${count}); do
echo "$newdir/$file"
done
}
record_captured_mail() {
local files=( $(get_captured_mail_files) )
local file
for file in ${files[@]}; do
record
record "[Captured mail file: $file]"
cat "$file" >>$TEST_OF 2>&1
done
}
sendmail_bv_send() {
# test sending mail, but don't actually send it...
local recpt="$1"
local timeout="$2"
local bvfrom from="$3"
# delivery status is emailed back to us, or 'from' if supplied
clear_postfix_queue
if [ -z "$from" ]; then
ensure_root_user
start_mail_capture "$ROOT"
else
bvfrom="-f $from"
start_mail_capture "$from"
fi
record "[Running sendmail -bv $bvfrom]"
sendmail $bvfrom -bv "$recpt" >>$TEST_OF 2>&1
if [ $? -ne 0 ]; then
test_failure "Error executing sendmail"
else
wait_mail $timeout || test_failure "Timeout waiting for delivery report"
fi
}
assert_python_success() {
local code="$1"
local output="$2"
record "$output"
record
record "python exit code: $code"
if [ $code -ne 0 ]; then
test_failure "unable to process mail: $(python_error "$output")"
return 1
fi
return 0
}
assert_python_failure() {
local code="$1"
local output="$2"
shift; shift
record "$output"
record
record "python exit code: $code"
if [ $code -eq 0 ]; then
test_failure "python succeeded but expected failure"
return 1
fi
local look_for
for look_for; do
if [ ! -z "$look_for" ] && ! grep "$look_for" <<< "$output" 1>/dev/null
then
test_failure "unexpected python failure: $(python_error "$output")"
return 1
fi
done
return 0
}