#!/bin/sh

##--------------------------------------------------------------
##
##  msmtpq-ng : sendmail compatible queue which sends mail using
##              msmtp light SMTP client
##  Concept based on msmtpqueue script by Martin Lambers
##  Copyright (C) 2008 - 2015 Chris Gianniotis
##  Largely rewritten by Daniel Dickinson 2016
##  Copyright (C) 2016-2019 Daniel Dickinson
##
##  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.
##
##--------------------------------------------------------------
##
## msmtpq-ng is meant to be used by an email client - in 'sendmail' mode
##   for this purpose, it can be invoked directly as 'msmtpq-ng'.
##   It also supports sendmail's -bs mode which allows, with the aid
##   aid of something like xinetd or systemd socket, use as an SMTP
##   server (no auth).
##
## there is a queue log file, distinct from the msmtp log,
##   for all events & operations on the msmtp queue
##
## (mutt users, using msmtpq-ng in 'sendmail' mode,
##  should make at least the following two settings in their .muttrc
##    set sendmail = /path/to/msmtpq-ng
##    set sendmail_wait = -1
##
##  please see the msmtp man page and docs for further mutt settings
##    and optimisations
## )
##
## Rewritten to be POSIX shell compliant and more sendmail
## compatible (esp. wrt to listing/managing queue) by
## Daniel Dickinson <cshored@thecshore.com> May-June 2016
##

## Global variables
LKD=""

[ -z "$MSMTP_NG_CMD" ] && MSMTP_NG_CMD="$0"

## Useful functions

dsp() {
	unset L
	for L; do
		[ -n "$L" ] && echo "  $L" || echo
	done
}

err() {
	dsp '' "$1" '' >&2
	if [ -n "$2" ]; then
		exit "$2"
	else
		exit 1
	fi
}

# Configuration files
#   System-wide defaults in /etc/msmtpq-ng.rc
#   User settings in ~/.msmtpq-ng.rc

if [ "$MSMTP_SKIP_CONF" != "true" ]; then
	# shellcheck source=./tmp/msmtpq-ng.rc
	[ -r "/etc/msmtpq-ng.rc" ] && . /etc/msmtpq-ng.rc
	# shellcheck source=./tmp/msmtpq-ng.rc
	[ -r ~/.msmtpq-ng.rc ] && . ~/.msmtpq-ng.rc
fi

if [ -n "$MSMTP_OVERRIDE_CONF" ]; then
	# shellcheck source=./tmp/msmtpq-ng.rc
	[ -r "$MSMTP_OVERRIDE_CONF" ] && . "$MSMTP_OVERRIDE_CONF"
	export MSMTP_OVERRIDE_CONF
fi

## only if necessary (in unusual circumstances - e.g. embedded systems),
##   enter the location of the msmtp executable  (no quotes !!)
##   e.g. ( MSMTP=/path/to/msmtp )
##   and uncomment the test for its existence
[ -z "$MSMTP" ] && MSMTP=msmtp
[ -n "$(command -v "$MSMTP")" ] || [ -x "$MSMTP" ] || {
	err "msmtpq-ng : can't find the msmtp executable [ $MSMTP ]"	# if not found - complain ; quit
}

## Secure log and spool settings
[ -z "$MSMTP_LOG_UMASK" ] && MSMTP_LOG_UMASK=077
[ -z "$MSMTP_UMASK" ] && MSMTP_UMASK=077

## the queue dir
[ -z "$Q" ] && Q=~/.msmtp.queue
if [ ! -d "$Q" ]; then
	if [ ! -d "$(dirname "${Q}")" ]; then
		# shellcheck disable=SC2174
		mkdir -m1777 -p "$(dirname "${Q}")"
	fi
	umask "$MSMTP_UMASK"
	mkdir -p "${Q}"
fi

if [ -z "$LOCK_CMD" ]; then
	LOCK_CMD=flock						# Default to flock
	[ -z "$(command -v "$LOCK_CMD")" ] && LOCK_CMD=lock	# If we don't have flock try busybox lock
	[ -z "$(command -v "$LOCK_CMD")" ] && {
		err "msmtpq-ng: Unable to find lock command" 72	# Exit with EX_OSFILE
	}
fi

# export all configuration globals so that commands
# run inside lock_queue work correctly
export MSMTP_OVERRIDE_CONF MSMTP Q LOG MAXLOGLEVEL MSMTP_LOG_UMASK MSMTP_UMASK MSMTP_HOLD_CLI_MAIL MSMTP_HOLD_MSMTP_MAIL MSMTP_SEND_DELAY MSMTP_MAX_QUEUE_LIFETIME MSMTP_LOCK_DIR MSMTPQ_NG_QUEUE EMAIL_CONN_TEST EMAIL_CONN_TEST_PING EMAIL_CONN_TEST_IP EMAIL_CONN_TEST_SITE LOCK_CMD
## set the queue log file var to the location of the msmtp queue log file
##   where it is or where you'd like it to be
##     ( note that the LOG setting could be the same as the )
##     ( 'logfile' setting in .msmtprc - but there may be   )
##     ( some advantage in keeping the two logs separate    )
##   if you don't want the log at all unset (don't just comment out) this var
##     #LOG=~/log/msmtp.queue.log  -->  LOG=""
##     (doing so would be inadvisable under most conditions, however)
##
## the queue log file - modify (or comment out) to taste  (but no quotes !!)
[ -z "$LOG" ] && LOG=~/log/msmtp.queue.log
[ -z "$MAXLOGLEVEL" ] && MAXLOGLEVEL=7		# Default to not displaying debug log messages
if [ -z "$LOG" ] || ! [ -d "${LOG%/*}" ]; then
	if [ "$LOG" != "syslog" ]; then
		err "msmtpq-ng : can't find msmtp log directory [ $(dirname $LOG) ]" # if not present - complain ; quit
	fi
fi

## Default to not holding command line mail
[ -z "$MSMTP_HOLD_CLI_MAIL" ] && MSMTP_HOLD_CLI_MAIL=false

## Default to holding SMTP mail (this helps avoid accidentally
## becoming an open relay
[ -z "$MSMTP_HOLD_SMTP_MAIL" ] && MSMTP_HOLD_SMTP_MAIL=true

## Only send messages marked hold older than number of seconds
[ -z "$MSMTP_SEND_DELAY" ] && MSMTP_SEND_DELAY=0

## Maximum queue lifetime (auto-purged after this, unless on hold)
[ -z "$MSMTP_MAX_QUEUE_LIFETIME" ] && MSMTP_MAX_QUEUE_LIFETIME=345600	# Four days

## Lock directory
[ -z "$MSMTP_LOCK_DIR" ] && MSMTP_LOCK_DIR=~/.msmtpq.lock
[ -d "$MSMTP_LOCK_DIR" ] || {
	if [ ! -d "$(dirname "${MSMTP_LOCK_DIR}")" ]; then
		# shellcheck disable=SC2174
		mkdir -m1777 -p "$(dirname "${MSMTP_LOCK_DIR}")"
	fi
	umask $MSMTP_UMASK
	mkdir -p "$MSMTP_LOCK_DIR"
}

## msmtpq-ng-queue binary
[ -z "$MSMTPQ_NG_QUEUE" ] && MSMTPQ_NG_QUEUE=msmtpq-ng-queue

## ======================================================================================

## msmtpq can use the following environment variables :
##   EMAIL_CONN_TEST     if =x will suppress any testing for a connection
##                       if =p or unset will use a ping test (debian.org) for a connection
##                       if =P will use a fast ping test (8.8.8.8) for a connection
##                       if =n will use netcat (nc) to test for a connection
##
[ -z "$EMAIL_CONN_TEST" ] && EMAIL_CONN_TEST=p
[ -z "$EMAIL_CONN_TEST_PING" ] && EMAIL_CONN_TEST_PING=debian.org
[ -z "$EMAIL_CONN_TEST_IP" ] && EMAIL_CONN_TEST_IP=8.8.8.8
[ -z "$EMAIL_CONN_TEST_SITE" ] && EMAIL_CONN_TEST_SITE=www.debian.org
## ======================================================================================

## write/remove queue lockfile on forced termination
on_exit() { # unlock the queue on exit if the lock was set here
	trap '' INT TERM EXIT				# clear trap so it doesn't happen again
	unset LOK
	LOK="${MSMTP_LOCK_DIR}/msmtpq-ng.lock"	# lock file name
	if [ "$(basename "$LOCK_CMD")" != "flock" ]; then
		[ -n "$LKD" ] && $LOCK_CMD -u "$LOK" && rm -f "$LOK"
	fi
	[ -n "$LKD" ] && rm -f "$LOK"			# Make sure to remove lock file
}

trap on_exit INT TERM EXIT				# run 'on_exit' on exit

umask $MSMTP_UMASK

usage() {
	echo "$(basename "$MSMTP_NG_CMD"): [options] -- <recipient>[ <recipient>]..."
	echo "$(basename "$MSMTP_NG_CMD"): [options] -t -- <recipient>[ <recipient>]..."
	echo ""
	# shellcheck disable=SC2039
	echo '-bp		List queue'
	# shellcheck disable=SC2039
	echo '-bs		SMTP (RFC821) mode'
	# shellcheck disable=SC2039
	echo '-i		Don'\''t use line with only a dot as end-of-mail indicator'
	# shellcheck disable=SC2039
	echo '-q		Flush queue'
	# shellcheck disable=SC2039
	echo '-qI <ID>	Flush a message with ID <ID>'
	# shellcheck disable=SC2039
	echo '--help		This message.'
	echo '<other>		Passed on to msmtp command'
	exit 1
}

## make an entry to the queue log file, possibly an error
##   (log queue changes only ; not interactive chatter)
## usage : log [ -e errcode ] msg [ msg ... ]
##  opts : -e <exit code>  an error ; log msg including error code
## display msg to user, as well
##
log() {
	unset ARG RC PFX PRI LOGLEVEL
	PRI="$1"						# Log priority
	shift

	if [ "$LOG" != "syslog" ]; then
		# time stamp prefix - "2008 13 Mar 03:59:45 "
		TIMEPFX="[$('date' +'%Y %d %b %H:%M:%S')] msmtpq-ng: "
		PFX="msmtpq-ng: "
		case "$PRI" in
	#	emerge)
	#		PFX="$PFX [EMERGENCY]"
	#		LOGLEVEL=1
	#		;;
	#	alert)
	#		PFX="$PFX [ALERT]"
	#		LOGLEVEL=2
	#		;;
	#	crit)
	#		PFX="$PFX [CRITICAL]"
	#		LOGLEVEL=3
	#		;;
		err)
			PFX="$PFX [ERROR]"
			LOGLEVEL=4
			;;
		warning)
			PFX="$PFX [WARNING]"
			LOGLEVEL=5
			;;	
		notice)
			PFX="$PFX [NOTICE]"
			LOGLEVEL=6
			;;
		info)
			PFX="$PFX [INFO]"
			LOGLEVEL=7
			;;
		debug)
			PFX="$PFX [DEBUG]"
			LOGLEVEL=8
			;;
		esac
		if [ "$LOGLEVEL" -gt "$MAXLOGLEVEL" ]; then
			return
		fi
	fi

	if [ "$1" = '-e' ] ; then				# there's an error exit code
		RC="$2"						# take it
		shift 2						# shift opt & its arg off
	fi

	case "$RC" in
	"64")
		EXITCODE="EX_USAGE: error in command line"
		;;
	"65")
		EXITCODE="EX_DATA: data format error"
		;;
	"66")
		EXITCODE="EX_NOINPUT: cannot open input"
		;;
	"67")
		EXITCODE="EX_NOUSER: addressee unknown"
		;;
	"68")
		EXITCODE="EX_NOHOST: hostname not known"
		;;
	"69")
		EXITCODE="EX_UNAVAILABLE: service unavailable"
		;;
	"70")
		EXITCODE="EX_SOFTWARE: internal software error"
		;;
	"71")
		EXITCODE="EX_OSERR: system error"
		;;
	"72")
		EXITCODE="EX_OSFILE: critical os file missing"
		;;
	"73")
		EXITCODE="EX_CANTCREAT: can't create (user) output file"
		;;
	"74")
		EXITCODE="EX_IOERR: i/o error"
		;;
	"75")
		EXITCODE="EX_TEMPFAIL: temporary failure"
		;;
	"76")
		EXITCODE="EX_PROTOCOL: remote error in protocol"
		;;
	"77")
		EXITCODE="EX_NOPERM: permission denied"
		;;
	"78")
		EXITCODE="EX_CONFIG: configuration error"
		;;
	""|"0")
		EXITCODE=""
		;;
	*)
		EXITCODE="unknown exit code = $RC"		# exit code for log
		;;
	esac

	if [ -n "$EXITCODE" ]; then
		ARG="$*"" ""$EXITCODE"
	else
		ARG="$*"
	fi


	umask $MSMTP_LOG_UMASK
	if [ -n "$ARG" ]; then
		[ "$MSMTP_QUEUE_QUIET" != "true" ] && \
			dsp "$PFX $ARG"				# display msg to user, as well as logging it
		if [ -n "$LOG" ] && [ "$LOG" != "syslog" ]; then	# log is defined and in use
			echo "$TIMEPFX $PFX : $ARG" >> "$LOG"	# line has content ; send it to log
		elif [ "$LOG" = "syslog" ]; then
			CURPID=$$
			echo "$ARG" | logger -t "msmtpq-ng[$CURPID]" -p mail."$PRI"
		fi
	fi
	umask $MSMTP_UMASK

}

#
# Prevent multiple process from accessing queue at once
#
# $@: Command and paramters to execute inside the lock
#
lock_err() {
	# Couldn't acquire lock
	log err -e 71 "cannot use queue $Q : Waited $MAX seconds to" \
		"acquire lock [ $LOK ], so giving up." \
		'If you are certain that no other instance of this script' \
		"is running then 'rm -f' the lock file manually: " ''
	exit 71						# exit with EX_OSERR
}

lock_queue() {
	unset LOK MAX SEC RC PARAMS OP
	LOK="${MSMTP_LOCK_DIR}/msmtpq-ng.lock"	# lock file name
	MAX=120					# max seconds to gain a lock
	SEC=0 					# seconds waiting

	if [ "$LOCK_CMD" = "flock" ]; then		# flock makes life easy
		while [ $SEC -lt $MAX ]; do
			flock -n "$LOK" "$MSMTP_NG_CMD" "$@"	# execute commands inside flock
			RC=$?
			if [ "$RC" = "1" ] && ! flock -n "$LOK" true; then # if it was a locking error
				sleep 1
			else
				SEC=120
				break
			fi
		done
		if [ "$RC" = "1" ] && [ "$SEC" = "120" ]; then
			lock_err
		fi
		rm -f "$LOK"				# make sure we don't have file with permissions
							# that blocks others users from locking
		return $RC				# return exit code of command
	else						# busybox lock
		touch "$LOK" 2>/dev/null
		$LOCK_CMD -n "$LOK" && LKD='t'
		while [ "$LKD" != 't' ] && [ $SEC -lt $MAX ]; do	# lock file present
			sleep 1					# wait a second
			SEC=$((SEC + 1))			# accumulate seconds
			touch "$LOK" 2>/dev/null
			$LOCK_CMD -n "$LOK" && LKD='t'
		done						# try again while locked for MAX secs
		[ "$LKD" != 't' ] && lock_err

		unset LKD
		"$MSMTP_NG_CMD" "$@"				# Execute command
		RC=$?
		$LOCK_CMD -u "$LOK"
		rm -f "$LOK"					# make sure we don't have file with permissions
								# that blocks others users from locking
		return $RC
	fi
}

## test whether system is connected
## returns t/f (0/1)
##
connect_test() {
	if [ -z "$EMAIL_CONN_TEST" ] || [ "$EMAIL_CONN_TEST" = 'p' ] ; then	# use ping test (default)
		# verify net connection - ping ip address of configured site
		# would ping -qnc2 -w4 be better ?
		# would ping -qnc1 -w10 or -w20 be better ?
		#ping -qnc1 -w4 debian.org >/dev/null 2>&1 || return 1
		ping -qnc2 -w10 $EMAIL_CONN_TEST_PING >/dev/null 2>&1 || return 1
	elif [ "$EMAIL_CONN_TEST" = 'P' ] ; then				# use quicker ping test
		# I personally think that including a DNS lookup
		# is a better connection test but some
		# have found the above test too slow
		ping -qnc1 -w4 $EMAIL_CONN_TEST_IP >/dev/null 2>&1 || return 1
	elif [ "$EMAIL_CONN_TEST" = 'n' ] ; then				# use netcat (nc) test
		# must, of course, have netcat (nc) installed
		command -v nc >/dev/null 2>&1 || {
			# if not found - complain ; quit
			log err -e 72 "can't find netcat [ nc ]"
			return 72
		}
		nc -vz $EMAIL_CONN_TEST_SITE 80 >/dev/null 2>&1 || return 1
	fi
	return 0
}

#
## send a queued mail out via msmtp
##
## $1: mail_id
send_queued_mail() {
	unset FQP
	unset RC
	FQP="${Q}/${1}"			# fully qualified path name
	RC=0			# for msmtp exit code
	unset RETMSG
	unset MSMTP_CMD
	unset FQPD
	unset FQPDLST

	# Handle multiple queues if root (otherwise only our own queue)
	if [ "$USER" = "root" ]; then
		unset FQP
		if [ -n "$(ls -A "$(dirname ${Q})"/*/*.mail 2>/dev/null)" ]; then
			FQPDLST="$(find "$(dirname "${Q}")" -maxdepth 1 -mindepth 1 -type d)"
			for FQPD in $FQPDLST; do
				FQP="${FQPD}/${1}"
				if [ -f "${FQP}.mail" ]; then
					break
				fi
			done
		else
			# Guess we're not on a multi-queue system
			FQP="${Q}/${1}"
		fi
	fi

	if [ -z "${FQP}" ]; then
		log warning -e 67 "message [ $1 ]; send failed; does not exist any queue in $(dirname "${Q}")"
		return 67
	fi

	# Held messages may not be sent until MSMTP_SEND_DELAY has elapsed
	if [ -e "${FQP}.hold" ]; then
		if [ "$(($(date +%s -r "${FQP}.mail" 2>/dev/null) + MSMTP_SEND_DELAY))" -gt "$(date +%s)" ]; then
			log info "message [ $1 ]; too new to unhold"
			echo "Too new" >"${FQP}".msg
			# This is a temporary error, so exit with EX_TEMPFAIL
			return 75
		fi
	fi

	if [ "$MSMTP_QUEUE_ONLY" = "true" ]; then
		log info "message [ $1 ]; can't be sent -- queueing only state"
		return 75
	fi

	if [ -f "${FQP}.msmtp" ] ; then			# corresponding .msmtp file found
		if [ "$EMAIL_CONN_TEST" != 'x' ]; then
			connect_test || {
				log notice "message [ $1 ]; couldn't be sent - host not connected"
				echo "Host not connected" >"${FQP}".msg
				# Connection error is temporary; exit with EX_TEMPFAIL
				return 75
			}
		fi

		# Attempt to send via msmtp
		if [ -n "$MSMTP_CONF" ]; then
			MSMTP_CMD="$MSMTP -C ""$MSMTP_CONF"" $(cat "${FQP}.msmtp")"
		else
			MSMTP_CMD="$MSMTP"" $(cat "${FQP}.msmtp")"
		fi
		$MSMTP_CMD < "${FQP}.mail" >"${FQP}".retmsg 2>&1
		RC=$?
		RETMSG="$(cat "${FQP}".retmsg)"
		rm -f "${FQP}".retmsg

		SMRC="$RC"
		if [ "$SMRC" = "0" ]; then			# this mail goes out the door
			# log success (and maybe display)
			log info "message [ $1 ]; send successful; purged from queue"
			[ -n "${FQP}" ] && [ -d "$(dirname "${FQP}")" ] && rm -f "${FQP}".*				# nuke all queue mail files
			# If message is reaches maximum time allowed in queue
		elif [ "$(($(date +%s -r "${FQP}.mail") + MSMTP_MAX_QUEUE_LIFETIME))" -le "$(date +%s)" ]; then
			# Purge it from the queue
			[ -n "${FQP}" ] && [ -d "$(dirname "${FQP}")" ] && rm -f "${FQP}".*				# nuke all queue mail files
			log warning "message [ $1 ] exceeded queue lifetime; purged"
		fi

		if [ "$SMRC" != "0" ]; then
			if echo "$RETMSG" | grep -qi 'no recipients found'; then
				if [ "$MSMTP_IGNORE_NO_RECIPIENTS" = "true" ]; then
					# Only log error if no recipients and ignoring
					log warning -e 67 "message [ $1 ]; send failed; no recipients"
					echo "No recipients" >"${FQP}".msg
					SMRC=0
				else
					# If we're not ignoring it's fatal and remove the mail
					log err -e 67 "message [ $1 ]; send failed ; no recipients; dequeued mail"
					[ -n "${FQP}" ] && [ -d "$(dirname "${FQP}")" ] && rm -f "${FQP}".*				# nuke all queue mail files
					SMRC=67
				fi
			else	# send was unsuccessful
				# For all other errors, log it and leaved queued
				log err -e "$SMRC" "message [ $1 ]; send failed"
				echo "$RETMSG" >"${FQP}".msg
			fi
		fi
		return $SMRC					# func returns exit code
	elif [ -f "${FQP}.mail" ]; then
		# corresponding param file not found
		log err "preparing to send .mail file [ $1 ] [ ${FQP}.mail ] but" \
			"  corresponding .msmtp file [ ${FQP}.msmtp ] was not found in queue" \
			'  skipping this mail ; this is worth looking into'
		echo "Missing .msmtp file" >"${FQP}".msg
	else
		log err "are you sure message-ID [ ""$1"" ] exists?"
	fi
	return 64
}

## run (flush) queue
##
run_queue() {
	unset M ID LST CURQ
	RQRC=0

	if [ "$MSMTP_QUEUE_ONLY" = "true" ]; then
		log info "Not running queue -- in queueing only mode"
	fi

	LST="$(ls "$Q"/*.mail 2>/dev/null)"		# list of mails in queue
	if [ "${USER}" = "root" ] && [ -n "$(ls -A "$(dirname "$Q")"/*/*.mail 2>/dev/null)" ]; then
		LST="$(ls "$(dirname "$Q")"/*/*.mail 2>/dev/null)" # list of mails in queues for all users
	fi
	HO=1

	if [ -n "$LST" ] ; then					# something in queue
		for M in $LST; do
		    CURQ="$(dirname "$M")"
		    ID="$(basename "$M" .mail)"
		    # Only send mail not marked hold
		    [ ! -e "${CURQ}/${ID}.hold" ] && {
			send_queued_mail "$ID"			# send mail - pass {id} only
			RC=$?
			if [ "$RC" != "0" ]; then
				[ "$RQRC" = "0" ] && RQRC="$RC"
			fi
			HO=0
		   }
                done
	fi

	# If hold only (i.e. no non-hold messages)
	if [ "$HO" = "1" ]; then
		[ "$MSMTP_QUEUE_QUIET" != "true" ] && \
			dsp '' 'mail queue is empty (nothing to send)' ''
	fi # inform user (if not running as MTA)
	return $RQRC
}

make_id() {
	unset INC ID FQP
	INC=0			# increment counter for (possible) base fqp name collision
					# we also include user id to avoid collision amongst queue
					# dirs

	INCSTR="$(printf "%03d" $INC)"
	ID="$(id -u).$(date +%Y%m%d%H%M)"	# make filename id for queue    (global
	FQP="${Q}/$ID"				# make fully qualified pathname  vars)
	# fqp name w/incr exists
	while [ -f "${FQP}${INCSTR}.mail" ] || [ -f "${FQP}${INCSTR}.msmtp" ]; do
		INC=$((INC + 1))	# bump increment
		INCSTR="$(printf "%03d" $INC)"
	done
	ID="${ID}${INCSTR}"			# unique ; set id
	FQP="${FQP}${INCSTR}"			# unique ; set fqp name
}

## enqueue a mail
##
## $@: Arguments to msmtp
##
enqueue_mail() {
	# Write command-line parameters to queue file
	echo "$@" >"${FQP}.msmtp"
	RC=$?
	if [ "$RC" != "0" ]; then
		log err -e "$RC" "creating msmtp cmd line file { $* }" \
		"           to [ ${ID}.msmtp ] : failed"
		echo "Failed to create .msmtp file; exit code = $RC" >"${FQP}".msg
		return $RC
	else
		log info "enqueued mail as : [ $ID ] ( $* ) : successful" # (queued .mail file is already there)
	fi
	return 0
}

## send a mail (enqueue it and attempt delivery, if possible)
## if send is successful, msmtp will also log it (if logging enabled in ~/.msmtprc)
##
## $@: Arguments to msmtp
##
send_mail() {
	unset RETMSG
	unset RC

	enqueue_mail "$@"		# We always queue so as not to duplicate send logic
	RC=$?

	if [ -e "${FQP}.hold" ]; then
		log info "mail for [ $* ] : to be held; queueing message"
		echo "Held" >"${FQP}".msg
		return $RC
	elif [ "$MSMTP_QUEUE_ONLY" = "true" ]; then
		log info "mail for [ $* ] : msmtpq set to queue only; queueing message"
		echo "Queue only" >"${FQP}".msg
		return $RC
	elif [ "$EMAIL_CONN_TEST" != 'x' ]; then
	       connect_test
	       CTRC=$?
	       if [ "$CTRC" != "0" ]; then
			log notice "mail for [ $* ] : couldn't be sent - host not connected"
			echo "Not connected" >"${FQP}".msg
			return 75
		fi
	fi

	if [ "$RC" != "0" ]; then
		return $RC
	fi

	send_queued_mail "${ID}"
	RC=$?
	return $RC
}

smtpd_out() {
	if [ "$KCOV_SMTPD_MODE" != "true" ]; then
		echo "$1"
	else
		echo "$1" >&4
	fi	
}

##
##
##
##
## Get sender from MAIL FROM
##
## $@: MAIL FROM line
##
## FROM: must be alterable by this function and accessible from caller
## Contains sender address
##
smtp_mail_from() {
	# sending mailbox
	unset from
	from="$(echo "$@"|sed -e '/^[Mm][Aa][Ii][Ll] [Ff][Rr][Oo][Mm]:<\([^@][^@]*@[^@][^@]*\)>/s/^[Mm][Aa][Ii][Ll] [Ff][Rr][Oo][Mm]:<\([^@][^@]*@[^@][^@]*\)>/\1/p
/^[Mm][Aa][Ii][Ll] [Ff][Rr][Oo][Mm]:<[^@][^@]*@[^@][^@]*>.*$/!d')"

	# Got return-path
	if [ -n "$from" ]; then
		smtpd_out "250 Ok"	# Success response
		log debug "[SMTP] Got return-path"
		return 0
	else
		# Bad input for MAIL FROM
		from=""
		smtpd_out "501 Syntax error in parameter or arguments"
		log err "[SMTP] Bad MAIL FROM: $line"
		return 1
	fi
}

smtp_handle_read() {
	line="$*"

	IFS="$oIFS"
	# First we need a HELO (we don't understand EHLO)
	if [ "$state" = "initial" ] && echo "$line" | grep -qi '^helo '; then
		state=mail
		smtpd_out "250 Ok"
		to=""
		from=""
		log debug "[SMTP] Got HELO"
		IFS='
'
		return			# next line
	elif [ "$state" = "initial" ]; then
		# We don't implement EHLO
		if echo "$line"|grep -qi '^ehlo '; then
			smtpd_out "502 Command not implemented"
			log debug "[SMTP] Got EHLO, we only understand HELO"
		# Any other command at this point is wrong
		elif test -z "$line"; then
			# ignore empty lines
			return	
		else
			smtpd_out "503 Command out of sequence"
			log err "[SMTP] Bad line $line expecting HELO"
		fi
		IFS='
'
		return			# next line
	fi

	# On QUIT exit the server
	if [ "$state" != "data" ] && echo "$line" | grep -qi '^quit'; then
		smtpd_out "221 $(cat /proc/sys/kernel/hostname) closing transmission channel"
		log debug "[SMTP] Got QUIT"
		return
	fi

	# We accept MAIL FROM until DATA is in progress
	if  [ "$state" = "mail" ] || [ "$state" = "rcpt" ]; then
		if echo "$line" | grep -qi '^mail from'; then
			if smtp_mail_from "$line"; then
				[ -n "${FQP}" ] && [ -d "$(dirname "${FQP}")" ] && rm -f "${FQP}".*				# nuke all queue mail files
				make_id				# make base queue filename id for this mail
				to=""
				state=rcpt
			else
				state=mail
				to=""
				from=""
				IFS='
'
			fi
			return				# next line
		fi
	fi
	# after data MAIL FROM starts a new transaction
	if  [ "$state" = "postdata" ] && echo "$line" | grep -qi '^mail from:<.*@.*>'; then
		make_id
		state=mail
		to=""
		FROM=""

		smtp_mail_from "$line" && {
			state=rcpt
			log debug "[SMTP] Got MAIL FROM ($FROM) after DATA"
		}
		IFS='
'
		return				# next line
	fi

	# We can reset at anytime except while receiving data
	if [ "$state" != "initial" ] && [ "$state" != "data" ] && echo "$line" | grep -qi '^rset'; then
		# reset state
		state=mail
		to=""
		from=""
		[ -n "${FQP}" ] && [ -d "$(dirname "${FQP}")" ] && rm -f "${FQP}".*				# nuke all queue mail files
		smtpd_out "250 Ok"			# response
		log debug "[SMTP] Got RSET"
		IFS='
'
		return					# next line
	fi

	# Got a recipient
	if [ "$state" = "rcpt" ] && echo "$line" | grep -qiE "^rcpt to:<.*@?.*>"; then
		unset newto
		newto="$(echo "$line"|sed -e '/^[Rr][Cc][Pp][Tt] [Tt][Oo]:<[^@][^@]*\(@[^@][^@]*\)\?>.*/s/^[Rr][Cc][Pp][Tt] [Tt][Oo]:<\([^@][^@]*\(@[^@][^@]*\)\?\)>.*/\1/p
/^[Rr][Cc][Pp][Tt] [Tt][Oo]:<[^@][^@]*\(@[^@][^@]*\)\?>.*/!d')"

		log debug "[SMTP] RCPT TO: $newto"

		# Bad input on RCPT TO
		if [ -z "$newto" ]; then
			smtpd_out "501 Syntax error in parameter or arguments"
			log err "[SMTP] Bad RCPT TO: $line"
			return			# next line
		fi

		if [ -z "$to" ]; then
			to="$newto"
		else
			to="$to $newto"
		fi

		smtpd_out "250 Ok"
		log debug "[SMTP] Got RCPT TO ($to)"
		IFS='
'
		return				# next line
	fi

	# Can do a NOOP at anytime
	if [ "$state" != "initial" ] && echo "$line" | grep -qi "^noop"; then
		smtpd_out "250 Ok"
		log debug "[SMTP] Got NOOP"
		IFS='
'
		return				# next line
	fi

	# Time for the actual data
	if [ "$state" = "rcpt" ] && echo "$line" | grep -qi "^data"; then
		state=data
		smtpd_out "354 Start of mail input; end with <CRLF>.<CRLF>"
		log debug "[SMTP] Got start DATA"
		IFS='
'
		return				# next line
	fi

	# We're in accept data mode
	if [ "$state" = "data" ]; then
		cleanline="$(echo "$line"|tr -d '\r')"
		# Then take from the beginning to line ending with .<CRLF>
		if ! echo "$cleanline"|grep -q '^\.$'; then
			echo "$cleanline"|sed -e 's/^\.\(..*\)$/\1/
s/^\([^.].*\)$/\1/' >>"${FQP}.mail"
			RC=$?
			# If there is error report it
			if [ "$RC" != "0" ]; then
				smtpd_out "554 Transaction Failed"
				log err -e $RC "[SMTP] mail for [ $from ] failed; failed to queue mail body"
				[ -n "${FQP}" ] && [ -n "${FQP}" ] && [ -d "$(dirname "${FQP}")" ] && rm -f "${FQP}".*				# nuke all queue mail files
				state=mail
			fi
			IFS='
'
			return	 	# next line
		fi
		log debug "Got $(wc -c "${FQP}.mail") bytes of DATA"

		# Record sender and recipients in msmtp command line form
		# shellcheck disable=SC2039
		PARAMS="-f ""$from"" -- ""$to"

		# We're finished getting data; now we'll send the mail and wait for
		# sender to end dialogue or start a new transaction
		state=postdata

		# If we want SMTP mail to be held until we manually release it...
		if [ "$MSMTP_HOLD_SMTP_MAIL" = "true" ]; then
			touch "${FQP}.hold"
		fi

		send_mail "$PARAMS"
		RC=$?

		# Success is fine, so is temporary failure because that means we've
		# queued and will retry later.
		if [ "$RC" = "0" ] || [ "$RC" = "75" ]; then
			smtpd_out "250 Ok"
			log debug "[SMTP] Message sent or queued"
			make_id				# make base queue filename id for next mail
		else
			# Otherwise report transaction failure
			smtpd_out "554 Transaction failed"
			log err -e $RC "[SMTP] Transaction failed"
			[ -n "${FQP}" ] && [ -n "${FQP}" ] && [ -d "$(dirname "${FQP}")" ] && rm -f "${FQP}".*	# nuke all queue mail files
			make_id				# make base queue filename id for next mail
		fi
	elif echo "$line" | grep -q '^$'; then
		return
	else
		if echo "$line" | grep -qiE '^(mail from|rcpt to|noop|rset|data|ehlo|helo|quit)'; then
			smtpd_out "503 Command out of sequence"
			log debug "[SMTP] Command out of sequence: $line"
		else
			smtpd_out "500 Syntax error, command unrecognized"
			log debug "[SMTP] Unrecognized command or syntax"
		fi
	fi
	IFS='
'
}

## Act as SMTP server (RFC821) on stdin/stdout
##
smtp_server() {
	unset oIFS
	unset state
	unset from to RC FROM ID FQP
	oIFS="$IFS"
	state="initial"

	# Initial greeting
	smtpd_out "220 $(cat /proc/sys/kernel/hostname) Simple Mail Transfer Service Ready"

	# Split on newline only
	IFS='
'
	if [ "$KCOV_SMTPD_MODE" != "true" ]; then
		while read -r line; do
			IFS="$oIFS"
			smtp_handle_read "$line"
			IFS='
'
		done
	else
		while read -r line; do
			IFS="$oIFS"
			smtp_handle_read "$line"
			IFS='
'
		done <&3
	fi
	IFS="$oIFS"
	return 0
}

##
## Sendmail command line queue and send
##
## $@: parameters to pass to msmtp
##
sendmail_queue() {
	unset SMQRC PARAMS ID FQP
	PARAMS="$*"
	make_id				# make base queue filename id for this mail

	if [ -z "$PARAMS" ]; then
		log warning -e 67 "No addressee!"
		return 67
	fi

	# write mail body text to queue .mail file
	if [ "$IGNORE_DOTS" = "true" ]; then
		cat > "${FQP}.mail"
		SMQRC=$?
	else
		sed -e '1,/^\.$/p
d
$d
s/^\.\(.+\)$/\1/' > "${FQP}.mail"
		SMQRC=$?
	fi

	if [ -r "${FQP}.mail" ] && [ ! -s "${FQP}.mail" ]; then
		# If there was no mail body assume caller didn't actually mean to send mail, even
		# if  cat/sed ended with error due to caller terminating before sending anything
		log debug "[sendmail] Empty body"
		[ -n "${FQP}" ] && [ -d "$(dirname "${FQP}")" ] && rm -f "${FQP}".*				# nuke all queue mail files
		return 0
	fi

	[ "$SMQRC" != "0" ] && {
		log err -e "$SMQRC" "creating mail body file [ $(basename "${FQP}").mail ] : failed"	# test for error
		return $SMQRC
	}

	# If we want command line mail to be held until we manually release it...
	if [ "$MSMTP_HOLD_CLI_MAIL" = "true" ]; then
		touch "${FQP}.hold"
	fi

	# Don't create queue file if we failed to create body
	if [ ! -s "${FQP}".mail ]; then
		rm -f "${FQP}".hold
		return $SMQRC
	fi

	send_mail "$PARAMS"			# queue and send the mail if possible
	SMQRC=$?
	return $SMQRC
}

#
## -- entry point
#

[ -z "$IGNORE_DOTS" ] && IGNORE_DOTS=false

unset PARAMS
RC=0

if [ "$(readlink -f "$(command -v "$1" 2>/dev/null)")" = "$(readlink -f "$(command -v "$MSMTP_NG_CMD")")" ]; then
	shift
fi

if [ -n "$MSMTP_WRAPPER" ] && [ "$(readlink -f "$(command -v "$2" 2>/dev/null)" 2>/dev/null)" = "$(readlink -f "$(command -v "$MSMTP_WRAPPER")")" ]; then
	shift
fi

if [ "$1" = "-lq" ]; then
	shift
	"$@"
	exit $?
fi

while [ -n "$1" ]; do
	OP="$1"
	shift
	case "$OP" in
	"-bp")
		exec $MSMTPQ_NG_QUEUE "-p" "$@"	# List queue (with any additional parameters)
		;;
	"-bs")
		if [ "$1" = "-kcov" ]; then
			shift
			KCOV_SMTPD_MODE=true
		fi
		export KCOV_SMTPD_MODE
		lock_queue -lq smtp_server	# Act as SMTP (RFC821) server on stdin/stdout
		exit 0
		;;
	 "-i")
		IGNORE_DOTS=true
		;;
	 "-q")
		lock_queue -lq run_queue
		exit $?
		;;
	"-qI")
		if [ "$1" = "ALL" ]; then
			lock_queue -lq run_queue
			exit $?
		else
			lock_queue -lq send_queued_mail "$@"
			RC=$?
			exit $RC
		fi
		;;
	"--help")
		usage
		;;
	*)
		if [ -z "$PARAMS" ];  then
			PARAMS="$OP"
		else
			PARAMS="$PARAMS"" ""$OP"
		fi
		;;
	esac
done

export IGNORE_DOTS
export KCOV_SMTPD_MODE
lock_queue -lq sendmail_queue "$PARAMS"
RC="$?"
exit "$RC"
