# Released under the terms of GPLv3 or at your option any later version.
# No warranties.
# Copyright 2008-2010 Enrico Tassi <gares@fettunta.org>

# Common stuff for smd-pull and smd-push
# Convention:
# - uppercase variables are global
# - lowercase variables are local
# - function arguments are documented assigning to local variable with
#   a decent name positional arguments

### Housekeeping ###

__TOREMOVE=""
__TOKILL=""

atexit_rm() {
	local path="$1"
	__TOREMOVE="$__TOREMOVE $path"
}

atexit_kill() {
	local pid="$1"
	__TOKILL="$__TOKILL $pid"
}

gc_mktemp() {
	local tmp=`mktemp -q /tmp/smd.XXXXXXXXXX`
	atexit_rm $tmp
	echo "$tmp"
}

__cleanup() {
	rm -f $__TOREMOVE
	for p in $__TOKILL; do
		kill $p 2>/dev/null || true
	done
}

trap __cleanup "EXIT"

### Variables setup and sanity checks ###

assert_executable() {
	if type $1 >/dev/null; then
		:
	else
		echo $1 not found, please install it or fix the paths
		echo PATH=$PATH
		echo type $1: `type $1`
		exit 1
	fi
}

init() {
	if [ `echo $PREFIX | cut -c -1` = "@" ]; then
		SMDSERVER=./smd-server
		SMDCLIENT=./smd-client
		MDDIFF=./mddiff
		# in development mode we assume that on the remote host
		# the software is installed such that binaries are in $PATH
		REMOTESMDSERVER=smd-server
		REMOTESMDCLIENT=smd-client
	else
		SMDSERVER=$PREFIX/bin/smd-server
		SMDCLIENT=$PREFIX/bin/smd-client
		MDDIFF=$PREFIX/bin/mddiff
		REMOTESMDSERVER="$SMDSERVER"
		REMOTESMDCLIENT="$SMDCLIENT"
	fi

	H=$HOME
	CONFDIR=$H/.smd
	LOCKFILE=$CONFDIR/lock
	SHOWTAGS=0
	VERBOSE=0
	DRYRUN=0
	TEMPLATE_ONLY=0
	CONFFILE=""
	WORKAREA=$CONFDIR/workarea
	REPNAME=default
	CHILDSARGS=
	REMOTEEXCLUDE=
	LOCALEXCLUDE=

	# default values for the configuration file
	DEBUG=false

	# external programs
	SSH="@SSH@"
	if [ `echo "$SSH" | cut -c -1` = "@" ]; then
		SSH=ssh
		echo "`basename $0` not installed, assuming secure shell client is $SSH"
	fi
	SED="@SED@"
	if [ `echo "$SED" | cut -c -1` = "@" ]; then
		SED=sed
		echo "`basename $0` not installed, assuming stream editor is $SED"
	fi

	# sanity checks for required binaries
	assert_executable $SED
	assert_executable $SSH
	assert_executable $MDDIFF
	assert_executable $SMDSERVER
	assert_executable $SMDCLIENT

	# setup of confdir
	mkdir -p $CONFDIR/
	mkdir -p $CONFDIR/log
	mkdir -p $CONFDIR/fifo
	mkdir -p $CONFDIR/hooks
	mkdir -p $CONFDIR/hooks/pre-pull.d
	mkdir -p $CONFDIR/hooks/pre-push.d
	mkdir -p $CONFDIR/hooks/post-pull.d
	mkdir -p $CONFDIR/hooks/post-push.d
	CRUFT=`find $CONFDIR/workarea ! -type d -a ! -type l 2> /dev/null || true`
	if [ ! -z "$CRUFT" ]; then
		echo "Some files are left in $CONFDIR/workarea"
		echo "This is an internal error. Please report this inconvenience"
		echo "and examine the content of these files, they may be of value."
		echo
		echo $CRUFT
		exit 1
	fi
	rm -rf $CONFDIR/workarea
	mkdir -p $CONFDIR/workarea
}

is_absolute() {
	case "$1" in
		/*) return 0 ;;
		*)	return 1 ;;
	esac
}

resolve_translator(){
local T=${1%% *}
if [ ! -z "$T" ]; then
	if is_absolute "$1"; then
		RC="$1"
	elif `type "$HOME/$T" >/dev/null 2>&1`; then
		RC="$HOME/$1"
	elif `type "$T" >/dev/null 2>&1`; then
		RC="$1"
	else
		echo "Unable to find the given translator: $T"
		echo "It is not an absolute path"
		echo "It is not in \$HOME=$HOME"
		echo "It is not in \$PATH=$PATH"
		if [ $showtags = 1 ]; then
			echo "$REPNAME: $localprog@$localhost: TAGS: error::context(conf) probable-cause(translator-not-found) human-intervention(necessary)"
		fi
		exit 1
	fi
else
	RC="cat"
fi
}

setup_workarea(){
	local TMP_FIND=`gc_mktemp`
	local TMP_FIND_ERR=`gc_mktemp`
	$MDDIFF $LOCALEXCLUDE -l $MAILBOX_LOCAL >$TMP_FIND 2>$TMP_FIND_ERR
	if [ ! $? -eq 0 ]; then
		echo "$MDDIFF gave an error while scanning $MAILBOX_LOCAL:"
		cat $TMP_FIND_ERR
		exit 1
	fi

	local FIFO_MKDIR="$CONFDIR/fifo/mkdir"
	[ -p "$FIFO_MKDIR" ] || mkfifo "$FIFO_MKDIR"
	$MDDIFF -s "$FIFO_MKDIR" &
	local DIR_MAKER=$!
	atexit_kill $DIR_MAKER
	exec 9>$FIFO_MKDIR

	local TMP_T=`gc_mktemp`
	local ERR=0
	$TRANSLATOR_LR <$TMP_FIND >$TMP_T || ERR=$?
	exec 7<$TMP_FIND
	exec 8<$TMP_T

   	while read M <&7 && read TM <&8; do
		if [ $ERR -eq 1 -o "$TM" = "ERROR" ]; then
			echo "Error: translating $M"
			cat $TMP_T
			if [ $showtags = 1 ]; then
				echo "$REPNAME: $localprog@localhost: TAGS: error::context(workarea) probable-cause(bad-translator) human-intervention(necessary)"
			fi
			exit 1
		fi
		if [ $VERBOSE -eq 1 ]; then
			echo "translating:" $M "->" $TM
		fi
		echo "$HOME/$M" 1>&9
		echo ".smd/workarea/$TM" 1>&9
	done

	exec 9>&-

	wait $DIR_MAKER || ERR=1
	if [ $ERR -eq 1 ]; then
		echo "Error: creating symlinks"
		if [ $showtags = 1 ]; then
			echo "$REPNAME: $localprog@localhost: TAGS: error::context(workarea) probable-cause(fail-create-symlink) human-intervention(necessary)"
		fi
		exit 1
	fi
}

### Command line argument parsing ###

parse_args() {
	for arg in "$@"; do
		case $arg in
			-v|--verbose)
				VERBOSE=1
				SHOWTAGS=1
			;;
			-s|--show-tags)
				SHOWTAGS=1
			;;
			-t|--template-only)
				TEMPLATE_ONLY=1
			;;
			-d|--dry-run)
				DRYRUN=1
				CHILDSARGS=-d
				VERBOSE=1
				SHOWTAGS=1
			;;
			-*)
				cat <<-EOT
				usage: `basename $0` [options] [endpoint]
				Refer to the man page for `basename $0`
				EOT
				exit 1
			;;
			*)
				REPNAME="$arg"
			;;

		esac
	done

	CONFFILE=$CONFDIR/config.$REPNAME
}

### Confdir setup ###

read_conffile() {
	# backward compatibility code
	if [ ! -f $CONFFILE ] && \
	   [ "$REPNAME" = "default" ] && \
	   [ -f $CONFDIR/config ]; then
		# we import the old conffile
		echo "From version 0.9.4, configuration files are named"
		echo "$CONFDIR/config.\$FOO, where FOO is an optional argument"
		echo "to smd-pull/smd-push. The default value of FOO is 'default'."
		echo "I'm renaming $CONFDIR/config to $CONFFILE."
		mv $CONFDIR/config $CONFFILE
	fi

	if [ ! -f $CONFFILE ]; then
		cat > $CONFFILE <<- EOT
		# No config file found, this is a template. You want to edit it.

		# Host name to be used with ssh as the server (use ~/.ssh/config
		# for extra options). smd-pull will pull from this host, smd-push
		# will push to this host and use it as the id of the remote mailbox.
		#
		# We suggest creating an alias with your ~/.ssh/config like:
		#
		#   Host smd-server-foo
		#     Compression yes
		#     Hostname your.real.server.name
		#     Username you
		#
		SERVERNAME=smd-server-$REPNAME

		# Host name to be used as the client.
		# smd-pull will use this just as an id for the client. If you
		# plan to sync with multiple endpoints, you must use a different
		# client id for any of them, thus a pair localhostname-remotehostname
		# should be used
		#
		CLIENTNAME=`hostname`-$REPNAME

		# The mailbox to sync, in case the path is the same on both hosts.
		# The path MUST be relative to the home directory, use a symlink if
		# the mailbox is not rooted there. If these paths contain spaces,
		# they must be substituted with %20.
		#
		MAILBOX="Mail/"

		# Local and remote mailbox may differ in name, as well as their
		# sub directory/folder structure. In that case a translator must be
		# provided. A translator is a program that takes in input, as it
		# first and only argument, a directory name (ending in /cur or /new
		# or /tmp) and prints on stdout its translation. Refer to the
		# smd-config (5) manpage for more infos.
		#
		# MAILBOX_LOCAL="Mail/"
		# MAILBOX_REMOTE="OtherMail/"
		# TRANSLATOR_RL=path
		# TRANSLATOR_LR=path

		# To exclude some paths from the synchronization you can specify
		# a space separated list of glob(7) expressions. Spaces in these
		# expressions must be replaced with %20.
		#
		# EXCLUDE="Mail/spam Mail/trash Mail/with%20spaces"
		#
		# If the local and remote mailbox differ in name or their
		# sub directory/folder structure you can specify different
		# excluded paths for the two endpoints.
		#
		# EXCLUDE_LOCAL="Mail/spam Mail/trash"
		# EXCLUDE_REMOTE="OtherMail/with%20spaces"

		# Log client to server and server to client communication.
		# This is useful only for debugging, since all network traffic
		# is dumped, including transmitted mails.
		#
		# DEBUG=true
		EOT
		echo No config file found: created a default one
		echo Please edit it: $CONFFILE
		if [ "$TEMPLATE_ONLY" = 1 ]; then
			exit 0
		else
			exit 1
		fi
	fi

	if [ "$TEMPLATE_ONLY" = 1 ]; then
		exit 0
	fi

	. $CONFFILE

	# sanityze
	MAILBOX="${MAILBOX%%/}"
	MAILBOX_LOCAL="${MAILBOX_LOCAL%%/}"
	MAILBOX_REMOTE="${MAILBOX_REMOTE%%/}"

	# default exclude
	if [ -z "$EXCLUDE_LOCAL" ]; then
			EXCLUDE_LOCAL="$EXCLUDE"
	fi
	if [ -z "$EXCLUDE_REMOTE" ]; then
			EXCLUDE_REMOTE="$EXCLUDE"
	fi
	for e in $EXCLUDE_LOCAL; do
			LOCALEXCLUDE="$LOCALEXCLUDE --exclude $e"
	done
	for e in $EXCLUDE_REMOTE; do
			REMOTEEXCLUDE="$REMOTEEXCLUDE --exclude $e"
	done

}

### Only one instance at a time please ###

check_lockfile() {
	# could be relaxed to non related mailboxes/enpoints, but the test is
	# not straightforward
	if [ -f $LOCKFILE ]; then
		if ps -p `cat $LOCKFILE` > /dev/null | grep -E 'smd-(push|pull)'; then
			echo Already running.
			echo If this is not the case, remove $LOCKFILE by hand.
			echo "any: smd-pushpull@localhost: TAGS: error::context(locking) probable-cause(another-instance-is-running) human-intervention(necessary) suggested-actions(run(kill `cat $LOCKFILE`) run(rm $LOCKFILE))"
			exit 1
		else
			echo Found lockfile of a dead instance. Ignored.
		fi
	fi

	echo $$ > $LOCKFILE
	atexit_rm $LOCKFILE
}

### Create all the needed pipes ###

setup_plumbing() {
	CtL=$CONFDIR/fifo/c2l.$REPNAME
	LtC=$CONFDIR/fifo/l2c.$REPNAME
	LtS=$CONFDIR/fifo/l2s.$REPNAME
	StL=$CONFDIR/fifo/s2l.$REPNAME

	[ -p $CtL ] || mkfifo $CtL
	[ -p $LtC ] || mkfifo $LtC
	[ -p $LtS ] || mkfifo $LtS
	[ -p $StL ] || mkfifo $StL
}

### Logging ###

__mycat() {
	# like cat, but ignores arguments
	cat
}

setup_logging() {
	CtS=$CONFDIR/log/c2s.$REPNAME.log
	StC=$CONFDIR/log/s2c.$REPNAME.log
	CL=$CONFDIR/log/client.$REPNAME.log
	SL=$CONFDIR/log/server.$REPNAME.log

	MITM=__mycat

	if [ "$DEBUG" = "true" ]; then
		MITM=tee
		CHILDSARGS="$CHILDSARGS -v"
	fi
}

setup_mailboxnames() {
	if [ -z "$MAILBOX" ]; then
		if [ -z "$MAILBOX_LOCAL" -o \
			 -z "$MAILBOX_REMOTE" -o \
			 -z "$TRANSLATOR_RL" -o \
			 -z "$TRANSLATOR_LR" ]; then
			echo "The config file must define MAILBOX xor MAILBOX_LOCAL, MAILBOX_REMOTE, TRANSLATOR_LR and TRANSLATOR_RL"
			exit 1
		fi
	else
		if [ ! -z "$MAILBOX_LOCAL" -o ! -z "$MAILBOX_REMOTE" ]; then
			echo "The config file must define MAILBOX xor MAILBOX_LOCAL, MAILBOX_REMOTE, TRANSLATOR_LR and TRANSLATOR_RL"
			exit 1
		fi
		MAILBOX_LOCAL="$MAILBOX"
		MAILBOX_REMOTE="$MAILBOX"
	fi
	resolve_translator "$TRANSLATOR_RL"
	TRANSLATOR_RL="$RC"
	resolve_translator "$TRANSLATOR_LR"
	TRANSLATOR_LR="$RC"
}

# this could be a system wide post-* hook
report() {
	local exitcode="$1"
	local showtags="$2"
	local currcmd="$3"
	local inversecmd="$4"
	local localprog="$5"
	local remoteprog="$6"
	if [ $VERBOSE = 1 ]; then
		grep ^INFO: $SL | sed 's/^INFO: //'
		grep ^INFO: $CL | sed 's/^INFO: //'
	fi
	if [ $exitcode = 1 ]; then
		grep ^ERROR $SL \
			| $SED "s/^/$remoteprog: /" \
			| $SED "s/@@INVERSECOMMAND@@/$inversecmd/" \
			| $SED "s/@@ENDPOINT@@/$REPNAME/"
		grep ^ERROR $CL \
			| $SED "s/^/$localprog: /" \
			| $SED "s/@@INVERSECOMMAND@@/$inversecmd/" \
			| $SED "s/@@ENDPOINT@@/$REPNAME/"
		grep ^ssh: $SL \
			| $SED "s/^/$remoteprog: ERROR: /"
	fi
	if [ $showtags = 1 ]; then
		#echo "`date`: $currcmd $SERVERNAME" >> $CL
		grep ^TAGS $SL \
			| $SED "s/^/$REPNAME: $remoteprog@$SERVERNAME: /" \
			| $SED "s/@@INVERSECOMMAND@@/$inversecmd/" \
			| $SED "s/@@ENDPOINT@@/$REPNAME/"
		grep ^TAGS $CL \
			| $SED "s/^/$REPNAME: $localprog@localhost: /" \
			| $SED "s/@@INVERSECOMMAND@@/$inversecmd/" \
			| $SED "s/@@ENDPOINT@@/$REPNAME/"
		if [ `grep ^TAGS $SL|wc -l` = 0 ] && \
		   [ `grep ^TAGS $CL|wc -l` = 0 ]; then
			# it may be that ssh failed to resolve the hostname
			# so we generate a fake tag for it
			cat $SL $CL
			echo "$REPNAME: $remoteprog@$SERVERNAME: TAGS: error::context(ssh) probable-cause(network) human-intervention(avoidable) suggested-actions(retry)"
		fi
	fi
}

### Hooks ###

run_hooks() {
	local dir="$1"
	local when="$2"
	local what="$3"
	local status="$4"
	for h in $dir/hooks/$when-$what.d/*; do
		if [ -x $h ]; then
			$h $when $what $REPNAME $status >> $CL 2>&1
		fi
	done
}


# vim:ts=4 filetype=sh:
