#!/bin/sh

###############################################################################
#
# Script to verify if unsupported map types are in use in the Postfix 
# configuration and if so provide recommendations for modifying the
# configuration file and rebuilding the maps where needed
#
# The script can be run without parameters, in which case it analyzes the
# configuration that postconf normally operates on when launched without -c
# parameter, usually /etc/postfix/main.cf and /etc/postfix/master.cf
#
# The script can be run with an optional parameter of the postfix config
# directory to be analyzed. In that case the script will look for main.cf 
# and master in the specified directory.
#
# KNOWN LIMITATIONS
# Does not correctly handle advice where a variable has been specified for
# the value of one of the cache maps
#
# The EXCLUDED_PARAMS may need to include more. At the moment it contains
# false positives that I identified.
#
###############################################################################

###############################################################################
# Configuration
###############################################################################

VERSION=0.1
UNSUPPORTED_MAP_TYPES="hash btree"

###############################################################################
# Executable locations
###############################################################################

X_GREP=/usr/bin/grep
X_POSTCONF=/usr/bin/postconf
X_SORT=/usr/bin/sort
X_TR=/usr/bin/tr
X_XARGS=/usr/bin/xargs
X_ALL="$X_GREP $X_POSTCONF $X_SORT $X_TR $X_XARGS"

###############################################################################
# Postfix parameters to skip when searching for included files since these
# parameters are expected to be directories or filenames which are not invovled
# in specifying database map files
###############################################################################

EXCLUDED_PARAMS="command_directory \
	command_execution_directory \
	config_directory \
	daemon_directory \
	data_directory \
	debugger_command \
	html_directory \
	mail_spool_directory \
	maillog_file_prefixes \
	mailq_path \
	manpage_directory \
	meta_directory \
	newaliases_path \
	queue_directory \
	readme_directory \
	sample_directory \
	sendmail_path \
	shlib_directory \
	smtp_tls_CAfile \
	smtp_tls_CApath \
	smtpd_tls_cert_file \
	smtpd_tls_key_file"

###############################################################################
# Postfix parameters that require read/write maps
###############################################################################

CACHE_PARAMS="address_verify_map \
	lmtp_tls_session_cache_database \
	postscreen_cache_map \
	smtp_connection_cache_destinations \
	smtp_sasl_auth_cache_name \
	smtp_tls_session_cache_database \
	smtpd_tls_session_cache_database"

###############################################################################
# FUNCTIONS
###############################################################################

###############################################################################
#
# Function to output values separated by whitespace line by line with an indent
#
# Input parameters:
# 	$INPUT the string to output
# 	$INDENT the line prefix (example "    ")
#
# Output parameter:
# 	None
#
###############################################################################

function output_list () {
	local V
	for V in $INPUT
	do
		echo "${INDENT}${V}"
	done
}

###############################################################################
#
# Function to check for unsupported map types in string
#
# Input parameters:
# 	$INPUT the string to search in
# 	$UNSUPPORTED_MAP_TYPES a space separated list of unsupported map types
#
# Output parameter:
# 	$RC - 0 for non found, 1 for found
#
###############################################################################

function unsupported_map_types () {
	RC=0
	for M in $UNSUPPORTED_MAP_TYPES
	do
		if [[ $INPUT =~ ^$M:|[^a-zA-Z]$M: ]]; then
			RC=1
		fi
	done
}

###############################################################################
#
# Function to pop first word from string 
#
# Input parameter:
# 	$FILES_TO_CHECK the string
#
# Output parameters:
# 	$F the first word
# 	$FILES_TO_CHECK the modified string without the first word
#
###############################################################################

function pop() {
	F="${FILES_TO_CHECK%% *}"
	if [ "$FILES_TO_CHECK" = "${FILES_TO_CHECK#* }" ]; then
		FILES_TO_CHECK=""
	else
		FILES_TO_CHECK="${FILES_TO_CHECK#* }"
	fi
}

###############################################################################
#
# Function to extract file names from parameter values
#
# Input parameter:
# 	$INPUT the parameter value to search
#
# Output parameter:
# 	$MATCH the space separated list of filenames (empty if none)
#
###############################################################################

function get_filename () {
	local W
	local V=`echo "$INPUT" | $X_TR , ' '`
	MATCH=
	for W in $V
	do	
		if [[ $W =~ ^(/[^, ]+) ]]; then
			MATCH="${MATCH}${BASH_REMATCH[1]} "
		fi
	done
}

###############################################################################
#
# Function to check if file exists and is readable
#
## Input parameter:
# 	$FILENAME the name of the file to be checked
#
# Output parameter:
# 	None: the function exits in case file is not found or not readable
#
###############################################################################

function is_file_readable () {
	if [ ! -f $FILENAME ]; then
		echo "File $FILENAME does not exit"
		exit 4
	fi
	if [ ! -r $FILENAME ]; then
		echo "File $FILENAME is not readable"
		exit 5
	fi
}

###############################################################################
#
# Function to recursively check parameter values for unsupported map types
# and included files
#
# Input parameters:
# 	$INPUT the value of the parameter
#	$P the parameter name
#	$WRITE_MAP 1 if it is a read/write map or 0 if read only map
#
# Output parameters:
# 	$ACTION_PARAMS_WRITE_MAPS parameter names whose values to update for read/write maps
# 	$ACTION_PARAMS_READ_MAPS parameter names whose values to update for read maps
# 	$ACTION_FILES_WRITE_MAPS file names whose contents to update for read/write maps
# 	$ACTION_FILES_READ_MAPS file names whose contents to update for read maps
#
###############################################################################

function check_value () {
	
	local FILES_CHECKED=
	
	# check for unsupported map types in the parameter value

	unsupported_map_types # input parameters $INPUT $UNSUPPORTED_MAP_TYPES output parameter $RC
	if [ $RC -eq 1 ]; then
        	if [ $WRITE_MAP -eq 1 ]; then
			ACTION_PARAMS_WRITE_MAPS="${ACTION_PARAMS_WRITE_MAPS}${P} "
		else
			ACTION_PARAMS_READ_MAPS="${ACTION_PARAMS_READ_MAPS}${P} "
		fi
	fi
	
	# check for files in the parameter value
	
	get_filename # input parameter $INPUT, output parameter $MATCH
	FILES_TO_CHECK=$MATCH

	while [ -n "$FILES_TO_CHECK" ]
	do
		# set F to first file and remove it from the list of FILES_TO_CHECK
		pop # input parameter: $FILES_TO_CHECK, output parameters $F $FILES_TO_CHECK

		# skip if file already analyzed
		if [[ $FILES_CHECKED =~ $F ]]; then
			continue
		fi

		FILES_CHECKED="${FILES_CHECKED}${F} "
		is_file_readable # input parameter $FILENAME
		# read in file contents
		INPUT=$(<$F)
		unsupported_map_types # input parameters $INPUT $UNSUPPORTED_MAP_TYPES output parameter $RC
		if [ $RC -eq 1 ]; then
      		  	if [ $WRITE_MAP -eq 1 ]; then
				ACTION_FILES_WRITE_MAPS="${ACTION_FILES_WRITE_MAPS}${F} "
			else
				ACTION_FILES_READ_MAPS="${ACTION_FILES_READ_MAPS}${F} "
			fi
		fi
		get_filename # input parameter $INPUT, output parameter $MATCH
		# add matches to list of FILES_TO_CHECK
		FILES_TO_CHECK="${FILES_TO_CHECK} ${MATCH}"
		FILES_TO_CHECK=`echo $FILES_TO_CHECK | $X_XARGS`
	done
}

###############################################################################
#
# Function to removed duplicates and external white spaces
#
# Input parameters:
# 	$ACTION_PARAMS_WRITE_MAPS parameter names whose values to update for read/write maps
# 	$ACTION_PARAMS_READ_MAPS parameter names whose values to update for read maps
#
# Output parameters:
# 	$ACTION_PARAMS_WRITE_MAPS parameter names whose values to update for read/write maps
# 	$ACTION_PARAMS_READ_MAPS parameter names whose values to update for read maps
#
###############################################################################

function normalize_actions_params () {
	ACTION_PARAMS_WRITE_MAPS=`echo $ACTION_PARAMS_WRITE_MAPS | $X_TR ' ' '\n' | $X_SORT -u | $X_XARGS`
	ACTION_PARAMS_READ_MAPS=`echo $ACTION_PARAMS_READ_MAPS | $X_TR ' ' '\n' | $X_SORT -u | $X_XARGS`
}

###############################################################################
#
# Function to removed duplicates and external white spaces
#
# Input parameters:
# 	$ACTION_FILES_WRITE_MAPS file names whose contents to update for read/write maps
# 	$ACTION_FILES_READ_MAPS file names whose contents to update for read maps
#
# Output parameters:
# 	$ACTION_FILES_WRITE_MAPS file names whose contents to update for read/write maps
# 	$ACTION_FILES_READ_MAPS file names whose contents to update for read maps
#
###############################################################################

function normalize_actions_files () {
	ACTION_FILES_WRITE_MAPS=`echo $ACTION_FILES_WRITE_MAPS | $X_TR ' ' '\n' | $X_SORT -u | $X_XARGS`
	ACTION_FILES_READ_MAPS=`echo $ACTION_FILES_READ_MAPS | $X_TR ' ' '\n' | $X_SORT -u | $X_XARGS`
}

###############################################################################
# ARGUMENT PROCESSING
###############################################################################

CONFIG_DIR=

if [ $# -gt 1 ]; then
	echo "Too many arguments specified"
	exit 1
fi
CONFIG_DIR=$1

###############################################################################
# INITIAL CHECKS AND SETUP
###############################################################################

###############################################################################
# If run with optional directory argument, check that the postfix configuration
# exists and is readable at that location and setup -c parameter for postconf
# commands
###############################################################################

if [ -n "$CONFIG_DIR" ]; then
	if [ ! -d $CONFIG_DIR ]; then
		echo "Directory $CONFIG_DIR does not exit"
		exit 2
	fi
	if [ ! -r $CONFIG_DIR ]; then
		echo "Directory $CONFIG_DIR is not readable"
		exit 3
	fi
	F_MAIN=$CONFIG_DIR/main.cf
	FILENAME=$F_MAIN
	is_file_readable # input parameter $FILENAME
	F_MASTER=$CONFIG_DIR/master.cf
	FILENAME=$F_MASTER
	is_file_readable # input parameter $FILENAME
	POSTCONF_CONFIG_PARAM="-c $CONFIG_DIR"
else
	POSTCONF_CONFIG_PARAM=
fi

###############################################################################
# Test executable files exist and are executable by the current user
###############################################################################

for F in $X_ALL
do
	if [ ! -f $F ]; then
		echo "File $F does not exit"
		exit 8
	fi
	if [ ! -x $F ]; then
		echo "File $F is not executable"
		exit 9
	fi
done

###############################################################################
# All postfix main.cf parameters
###############################################################################

ALL_PARAMS=`$X_POSTCONF $POSTCONF_CONFIG_PARAM -H`

###############################################################################
# All postfix master.cf commands
###############################################################################

declare -a ALL_COMMANDS
MASTER=`$X_POSTCONF $POSTCONF_CONFIG_PARAM -F | $X_GREP '/command = ' | $X_GREP '\-o'`
SAVED_IFS=$IFS
IFS=$'\n'
for L in $MASTER; do
	ALL_COMMANDS+=($L)
done
IFS=$SAVED_IFS


###############################################################################
# Check if ${default_database_type} and ${default_cache_db_type} parameters are
# supported
###############################################################################

if [[ $ALL_PARAMS =~ 'default_database_type' ]]; then
	SUPPORTED_DEFAULT_DATABASE_TYPE=1
else
	SUPPORTED_DEFAULT_DATABASE_TYPE=0
fi

if [[ $ALL_PARAMS =~ 'default_cache_db_type' ]]; then
	SUPPORTED_DEFAULT_CACHE_DB_TYPE=1
else
	SUPPORTED_DEFAULT_CACHE_DB_TYPE=0
fi

###############################################################################
# MAIN ROUTINE
###############################################################################

echo "CONFIGURATION ANALYSIS"
echo "----------------------"
echo "(this may take a minute)"
echo

ACTION_FILES_WRITE_MAPS=
ACTION_FILES_READ_MAPS=
ACTION_PARAMS_WRITE_MAPS=
ACTION_PARAMS_READ_MAPS=

for P in $ALL_PARAMS
do
	# skip if unsupported parameter

	if [[ $EXCLUDED_PARAMS =~ $P ]]; then
		continue
	fi

	# determine if the parameter is for a read/write map

        if [[ $CACHE_PARAMS =~ $P ]]; then
		WRITE_MAP=1
	else
		WRITE_MAP=0
	fi
	
	# lookup parameter value

	INPUT=`$X_POSTCONF $POSTCONF_CONFIG_PARAM -h $P`

	# Recursively check parameter values for unsupported map types
	# and included files

	# Input parameters:
	# 	$INPUT the value of the parameter
	#	$P the parameter name
	#	$WRITE_MAP 1 if it is a read/write map or 0 if read only map
	#
	# Output parameters:
	# 	$ACTION_PARAMS_WRITE_MAPS parameter names whose values to update for read/write maps
	# 	$ACTION_PARAMS_READ_MAPS parameter names whose values to update for read maps
	# 	$ACTION_FILES_WRITE_MAPS file names whose contents to update for read/write maps
	# 	$ACTION_FILES_READ_MAPS file names whose contents to update for read maps

	check_value
done
normalize_actions_params

INDENT="    "
echo
echo "main.cf PARAMETERS"
echo "------------------"
echo
if [ -n "$ACTION_PARAMS_WRITE_MAPS" ]; then
	echo 'Review the contents of the following parameters and update unsupported btree map types to'
	echo '${default_cache_db_type} from Postfix >= 3.11 or for earlier versions to a map type'
	echo 'such as lmtp or sdbm providing it is supported on the target system:'
	if [ $SUPPORTED_DEFAULT_CACHE_DB_TYPE -eq 0 ]; then
		echo 'Warning: the current system does not support ${default_cache_db_type}. Before using'
		echo 'it check if it is available on the target system (if it is not the current system)'
		echo 'and if unavailable use an explicit map type that is supported.'
	fi
	echo
	INPUT=$ACTION_PARAMS_WRITE_MAPS
	output_list # input parameter $INPUT $INDENT
	echo
fi
if [ -n "$ACTION_PARAMS_READ_MAPS" ]; then
	echo 'Review the contents of the following parameters and update unsupported hash map types to'
	echo '${default_database_type} or to a map type such as lmtp or cdb providing it is'
	echo 'supported on the target system:'
	if [ $SUPPORTED_DEFAULT_DATABASE_TYPE -eq 0 ]; then
		echo 'Warning: the current system does not support ${default_database_type}. Before using'
		echo 'it check if it is available on the target system (if it is not the current system)'
		echo 'and if unavailable use an explicit map type that is supported.'
	fi
	echo
	INPUT=$ACTION_PARAMS_READ_MAPS
	output_list # input parameter $INPUT $INDENT
	echo
	echo "and then run postmap on the maps (or postalias in the case of maps included in alias_maps)"
	echo
fi
if [ -z "$ACTION_PARAMS_WRITE_MAPS" ] && [ -z "$ACTION_PARAMS_READ_MAPS" ]; then
	echo "No action identified"
	echo
fi

ACTION_PARAMS_WRITE_MAPS=
ACTION_PARAMS_READ_MAPS=
r='^([^/]+)/+[^ ]+ = [^ ]+(.*)$'
r2='^([^=]+)=(.*)'
for index in ${!ALL_COMMANDS[@]}
do
	L="${ALL_COMMANDS[index]}"
	if [[ $L =~ $r ]]; then
		P="${BASH_REMATCH[1]}"
		XS="${BASH_REMATCH[2]}"
		for X in $XS
		do
			if [ "$X" = "-o" ]; then
				continue
			else
				if [[ $X =~ $r2 ]]; then
					PARAM="${BASH_REMATCH[1]}"
					INPUT="${BASH_REMATCH[2]}"
					
					# determine if the parameter is for a read/write map
				        if [[ $CACHE_PARAMS =~ $PARAM ]]; then
						WRITE_MAP=1
					else
						WRITE_MAP=0
					fi
					check_value
				fi
			fi
		done
	fi
done
normalize_actions_params
normalize_actions_files

echo
echo "master.cf PARAMETERS"
echo "--------------------"
echo
if [ -n "$ACTION_PARAMS_WRITE_MAPS" ]; then
	echo 'Review the contents of the following master.cf entries and update unsupported btree map types to'
	echo '${default_cache_db_type} from Postfix >= 3.11 or for earlier versions to a map type'
	echo 'such as lmtp or sdbm providing it is supported on the target system:'
	if [ $SUPPORTED_DEFAULT_CACHE_DB_TYPE -eq 0 ]; then
		echo 'Warning: the current system does not support ${default_cache_db_type}. Before using'
		echo 'it check if it is available on the target system (if it is not the current system)'
		echo 'and if unavailable use an explicit map type that is supported.'
	fi
	echo
	INPUT=$ACTION_PARAMS_WRITE_MAPS
	output_list # input parameter $INPUT $INDENT
	echo
fi
if [ -n "$ACTION_PARAMS_READ_MAPS" ]; then
	echo 'Review the contents of the following master.cf entries and update unsupported hash map types to'
	echo '${default_database_type} or to a map type such as lmtp or cdb providing it is'
	echo 'supported on the target system:'
	if [ $SUPPORTED_DEFAULT_DATABASE_TYPE -eq 0 ]; then
		echo 'Warning: the current system does not support ${default_database_type}. Before using'
		echo 'it check if it is available on the target system (if it is not the current system)'
		echo 'and if unavailable use an explicit map type that is supported.'
	fi
	echo
	INPUT=$ACTION_PARAMS_READ_MAPS
	output_list # input parameter $INPUT $INDENT
	echo
	echo "and then run postmap on the maps (or postalias in the case of maps included in alias_maps)"
	echo
fi
if [ -z "$ACTION_PARAMS_WRITE_MAPS" ] && [ -z "$ACTION_PARAMS_READ_MAPS" ]; then
	echo "No action identified"
	echo
fi

echo
echo "FILES"
echo "-----"
echo 
if [ -n "$ACTION_FILES_WRITE_MAPS" ]; then
	echo 'Review the contents of the following files and update unsupported btree map types to'
	echo '${default_cache_db_type} from Postfix >= 3.11 or for earlier versions to a map type'
	echo 'such as lmtp or sdbm providing it is supported on the target system:'
	if [ $SUPPORTED_DEFAULT_CACHE_DB_TYPE -eq 0 ]; then
		echo 'Warning: the current system does not support ${default_cache_db_type}. Before using'
		echo 'it check if it is available on the target system (if it is not the current system)'
		echo 'and if unavailable use an explicit map type that is supported.'
	fi
	echo
	INPUT=$ACTION_FILES_WRITE_MAPS
	output_list # input parameter $INPUT $INDENT
	echo
fi
if [ -n "$ACTION_FILES_READ_MAPS" ]; then
	echo 'Review the contents of the following files and update unsupported hash map types to'
	echo '${default_database_type} or to a map type such as lmtp or cdb providing it is'
	echo 'supported on the target system:'
	if [ $SUPPORTED_DEFAULT_DATABASE_TYPE -eq 0 ]; then
		echo 'Warning: the current system does not support ${default_database_type}. Before using'
		echo 'it check if it is available on the target system (if it is not the current system)'
		echo 'and if unavailable use an explicit map type that is supported.'
	fi
	echo
	INPUT=$ACTION_FILES_READ_MAPS
	output_list # input parameter $INPUT $INDENT
	echo
	echo "and then run postmap on the maps (or postalias in the case of maps included in alias_maps)"
fi
if [ -z "$ACTION_FILES_WRITE_MAPS" ] && [ -z "$ACTION_FILES_READ_MAPS" ]; then
	echo "No action identified"
	echo
fi
