#!/bin/sh
# -*- indent-tabs-mode: nil; -*-
: 'A simple replacement for Bacula `bsmtp` when the underlying mailer does
not listen on TCP ports (e.g. `dma`, `ssmtp` et al.).

:Author:    Franz Glasner
:Copyright: (c) 2019-2023 Franz Glasner.
            All rights reserved.
:License:   BSD 3-Clause "New" or "Revised" License.
            See LICENSE for details.
            If you cannot find LICENSE see
            <https://opensource.org/licenses/BSD-3-Clause>
:ID:        @(#)@@SIMPLEVERSIONTAG@@

'

VERSION="@@VERSION@@"

USAGE='
USAGE: bsmtp2dma [OPTIONS] RECIPIENT ...

Options:

  -V           Show the program version and usage and exit.

  -8           Does nothing. Just a compatibility option for `bsmtp`.

  -c ADDRESS   Set the "CC:" header.

  -d n         Does nothing. Just a compatibility option for `bsmtp`.

  -f ADDRESS   Set the "From:" header.

  -h MAILHOST:PORT  Does nothing. Just a compatibility option for `bsmtp`.

  -l NUMBER    Does nothing. Just a compatibility option for `bsmtp`.

  -r ADDRESS   Set the "Reply-To:" header

  -s SUBJECT   Set the "Subject:" header


Files:

  The shell style configuration file in 

      @@ETCDIR@@/bsmtp2dma.conf is

  sourced in at script start.

'

#
# Configuration directory
#
: ${CONFIGDIR:=@@ETCDIR@@}

test -r "${CONFIGDIR}/bsmtp2dma.conf" && . "${CONFIGDIR}/bsmtp2dma.conf"


#
# Default configuration values
#
# `sendmail` is also valid for `dma` because of the mapping within
#  `/etc/mail/mailer.conf`
#
: ${MAILER:=/usr/sbin/sendmail}


parse_addr() {
    : 'Parse an possibly complex email address.

    Addresses can be of the form

    - Name Parts <user@domain.tld>
    - user@domain.tld

    `Name Parts` may not contain ``<`` or ``>`` characters.

    Args:
        _addr: the complex email address

    Returns:
        0 on success, 1 on errors

    Output (Globals):
        email_name: the name part (or empty)
        email_addr: the technical address part (or empty)

    '
    local _addr

    _addr="$1"
    test -n "${_addr}" || return 1

    if printf "%s" "${_addr}" | grep -q -E -e '^[^<>]+<[^<>]+@[^<>]+>$'; then
        email_name=$(printf '%s' "${_addr}" | sed -E -e 's/[[:space:]]*<.+$//')
        email_addr=$(printf '%s' "${_addr}" | sed -E -e 's/^[^<>]+<//' | sed -E -e 's/>$//')
        return 0
    fi
    if printf "%s" "${_addr}" | grep -q -E -e '^[^<>]+@[^<>]+$'; then
        email_name=""
        email_addr="${_addr}"
        return 0
    fi
    return 1
}


send_mail() {
    : 'Send the mail via the underlying configured mailer (dma, sendmail et al.).

    Args:
        _recipient: The recipient name.

                    Will be written into the "To:" header also.

    Input (Globals):
        MAILER
        MAILCONTENT
        MAILFIFO_STDIN
        MAILFIFO_STDOUT
        CC
        FROM
        REPLYTO
        SUBJECT

    Returns:
        0 on success, other values on errors or the error exit code from the
        underlying mailer

    This procedure starts the configured mailer as coproc and sends
    email headers and contents to the started mailer.

    '
    local _recipient _rc _oifs _text _pid_mailer _recipient_addr
    local _from_from _from_addr _sender_addr _dummy

    _recipient="$1"
    _rc=0

    if parse_addr "${_recipient}"; then
        _recipient_addr="${email_addr}"
    else
        echo "ERROR: unknown recipient address format in \`${_recipient}'" >&2
        return 1
    fi
    _sender_addr="$(whoami)@$(hostname -f)"
    if [ -z "${FROM}" ]; then
        _from_addr="${_sender_addr}"
        _from_from="${_from_addr}"
    else
       if parse_addr "${FROM}"; then
           _from_from="${FROM}"
           _from_addr="${email_addr}"
       else
           echo "ERROR: unknown sender name in \`${FROM}'" >&2
           return 1
       fi
    fi

    mkfifo -m 0600 "${MAILFIFO_STDIN}"
    _rc=$?
    if [ ${_rc} -ne 0 ]; then
        return ${_rc}
    fi
    mkfifo -m 0600 "${MAILFIFO_STDOUT}"
    _rc=$?
    if [ ${_rc} -ne 0 ]; then
        rm -f "${MAILFIFO_STDIN}"
        return ${_rc}
    fi

    #
    # Start the mailer **before** opening the pipe; otherwise a
    # deadlock occurs
    #
    "$MAILER" -f "${_sender_addr}" "${_recipient_addr}" <${MAILFIFO_STDIN} >${MAILFIFO_STDOUT} &
    _pid_mailer=$!

    exec 3>"${MAILFIFO_STDIN}"
    exec 4<"${MAILFIFO_STDOUT}"

    printf "To: %s\n" "${_recipient}" >&3
    printf "From: %s\n" "${_from_from}" >&3
    if [ "${_sender_addr}" != "${_from_addr}" ]; then
        printf "Sender: %s\n" "${_sender_addr}" >&3
    fi
    if [ -n "${SUBJECT}" ]; then
        printf "Subject: %s\n" "${SUBJECT}" >&3
    fi
    if [ -n "${REPLYTO}" ]; then
        #
        # XXX TBD proper Reply-To header value checks:
        #     a comma separated list of full mail addresses
        #
        printf "Reply-To: %s\n" "${REPLYTO}" >&3
    fi
    if [ -n "${CC}" ]; then
        #
        # XXX TBD proper CC header value checks:
        #     a comma separated list of full mail addresses
        #
        printf "Cc: %s\n" "${CC}" >&3
    fi
    printf "\n" >&3

    # preserve leading white space when reading with `read`
    _oifs="$IFS"
    IFS="
"
    cat "${MAILCONTENT}" |
        while read _text; do
            printf "%s\n" "$_text" >&3
        done
    # not all mailer recognize this
    # printf ".\n" >&3
    IFS="$_oifs"

    # close the fd to the pipe: coproc should get EOF and terminate
    exec 3>&-
    # read eventually remaining stuff from the mailer until EOF
    IFS='' read _dummy <&4
    exec 4<&-

    wait $_pid_mailer
    _rc=$?

    # we are done with the named pipes
    rm -f "${MAILFIFO_STDIN}"
    rm -f "${MAILFIFO_STDOUT}"

    return ${_rc}
}


while getopts "V8c:d:f:h:l:nr:s:" _opt; do
    case ${_opt} in
        V)
            printf 'bsmtp2dma %s\n' '@@SIMPLEVERSIONSTR@@'
            echo "$USAGE"
            exit 0;
            ;;
        8)
            : # VOID
            ;;
        c)
            CC="$OPTARG"
            ;;
        d)
            : # VOID
            ;;
        f)
            FROM="$OPTARG"
            ;;
        h)
            : # VOID
            ;;
        l)
            : # VOID
            ;;
        r)
            REPLYTO="$OPTARG"
            ;;
        s)
            SUBJECT="$OPTARG"
            ;;
        \?)
            exit 2;
            ;;
        *)
            echo "ERROR: inconsistent option handling" >&2
            exit 2;
            ;;
    esac
done

# return code
_rc=0

MAILTMPDIR="$(mktemp -d)"
MAILFIFO_STDIN="${MAILTMPDIR}/mail-stdin"
MAILFIFO_STDOUT="${MAILTMPDIR}/mail-stdout"
MAILCONTENT="${MAILTMPDIR}/mail-text"

#
# Clean up existing temporary stuff on all sorts of exit
# (including the "exit" call (signal 0))
#
trap 'if [ -d "${MAILTMPDIR}" ]; then rm -rf "${MAILTMPDIR}"; fi; exit;' 0 1 2 15

test -d "${MAILTMPDIR}" || { echo "ERROR: no existing private tmp dir" >&2; exit 1; }

#
# Reset the Shell's option handling system to prepare for handling
# other arguments and probably command-local options
#
shift $((OPTIND-1))
OPTIND=1

# early check whether some recipients are given
if [ $# -eq 0 ]; then
    echo "ERROR: no recipient given" >&2
    exit 2;
fi

#
# Collect the mail text from stdin into a temporary file
#
exec 3>"${MAILCONTENT}"
# preserve leading white space when reading with `read`
_oifs="$IFS"
IFS="
"
while read _text; do
    if [ "${_text}" = "." ]; then
        break
    else
        printf "%s\n" "${_text}" >&3
    fi
done
exec 3>&-
IFS="$_oifs"

#
# Now send the content of the collected mail content to all recipients
#
until [ $# -eq 0 ]; do
    send_mail "$1"
    _rcsm=$?
    if [ \( ${_rcsm} -ne 0 \) -a \( ${_rc} -eq 0 \) ]; then
        _rc=${_rcsm}
    fi
    shift
done

exit ${_rc}
