#! /bin/bash
#
# -- pine-gpg-filter-0.6, 2006-07-07; Martin A. Brown <martin@linux-ip.net>
#
# -- released under the GPL
#
# -- A pine filter which can handle multiple GPG roles.
#
# -- ChangeLog
#
#    0.6 2006-07-07; -MAB
#        wrote quite a bit of introductory (README) documentation
#    0.5 2006-07-06; -MAB
#        version bump just before attempting autoid work
#    0.4 2006-07-05; -MAB
#        oops!  Forgot that it's supposed to be "clearsign"; fixed
#        added logic switches so that it can be called by many names
#    0.3 2006-07-04; -MAB
#        improved documentation (long_usage)
#        verify functionality needs to use "tee", "mkfifo"
#        functionizing verify, encrypt, sign and so forth
#    0.2 2006-07-04; -MAB
#        fixed quoting issues for "$PGF_COMMENT"
#        shorter default comment string
#        put recipient existence check in correct place (--encrypt only)
#        allow user to specify the (--quiet) option
#    0.1 2006-07-04; -MAB
#        initial version; great U.S. Independence Day activity
#
# -- Contributors
#
#    Mike Pomraning <mjp@pilcrow.madison.wi.us>;  MJP supplied an
#    explanation for file pointer behaviour upon child process exit.
#    In short, a program (e.g., shell) has no way of knowing where a
#    child process has left the file pointer, if the child process
#    simply exits.  For ramifications, see --autoid code.
#
# -- BUGS/TODO
#
#    - next version needs to be able to automatically figure out the
#      local-user ID to employ when sending

# -- script global variables
#
VERBOSE="${PGF_VERBOSE:-0}"
INFORM=1
NOTICE=2
DEBUG=3

STDOUT=1
STDERR=2

VERSION=0.6
NAME=pine-gpg-filter
PGF_URL="http://linux-ip.net/sw/pine-gpg-filter/"

REQUIRED_UTILITIES="gpg mkfifo rm tee"

# -- remove these from our environment for sanity
#
unset GPG_OPT_LU            \
      GPG_OPT_RECIPIENTS    \
      PGF_RECIPIENTS        \
      PGF_MODE              \
      PGF_TO_SELF

# -- set up some global defaults, but allow for environment variables
#
 : "${PGF_COMMENT:=pgf-$VERSION ($PGF_URL)}"
 : "${PGF_STATLINES:=multi}"

# -- and set up some internal flags for the different primary actions
#
unset C S E V D
C="--clearsign"
S="--sign"
E="--encrypt"
D="--decrypt"
V="--verify"


# -- my favorite shell functions (and their offspring)
#
# - - - - - - - - - 
  gripe () {
# - - - - - - - - - 
#
  local descriptor=$1   && shift
  printf -- "%s\n" "$@" >&${!descriptor}
}

abort  () { gripe STDERR "$@"; exit 1;     }

inform () { test "$VERBOSE" -ge "$INFORM" && gripe STDERR "I: $@"; }
notice () { test "$VERBOSE" -ge "$NOTICE" && gripe STDERR "N: $@"; }
debug  () { test "$VERBOSE" -ge "$DEBUG"  && gripe STDERR "D: $@"; }

# -- create logger as a shell function
#
logger () { command logger -i -t "${0##*/}[$$]" -p daemon.err -- "$@" ; }

# - - - - - - - - - - -
  version  () {
# - - - - - - - - - - -
#
  gripe STDOUT "${0##*/}-${VERSION}"
  exit 0
}

# - - - - - - - - - - -
  usage  () {
# - - - - - - - - - - -
#
  local descriptor=$1   && shift
  cat >&${!descriptor} <<-EOUSAGE
Usage: ${NAME} [ options ] [ <recipient> ... ]

Options (defaults marked, or in parentheses):
  -h, --help                  Print out this handy help screen.
  -L, --long-usage            Provide help and some more hints.
  -q, --quiet                 Reset verbosity to none.
  -v, --verbose               Increase verbosity, can be used multiple times.
  -V, --version               Print version information.
  -c, --clearsign             Ask GPG to clearsign the input.
  -e, --encrypt               Ask GPG to encrypt the input.
  -s, --sign                  Ask GPG to sign the input.
  -d, --decrypt               Ask GPG to decrypt the input.
  -y, --verify                Ask GPG to verify the input.
  -o, --one-line              Compress multiline status output from GPG.
  -m, --multi-line            Allow all of the status output from GPG.
  -t, --to-self               Also encrypt-to-self (ignored if not needed).
  -n, --no-comment            Suppress any comment string.
  -C, --comment     <string>  Override the default comment string.
  -D, --datafile    <file>    Use pine-provided _DATAFILE_ as temporary FIFO.
  -R, --resultfile  <file>    Pine reports command output via _RESULTFILE_.
  -l, --local-user  <key_id>  Specify a local username.

EOUSAGE
}

# - - - - - - - - - - -
  short_usage  () {
# - - - - - - - - - - -
#
  local descriptor="${1:-STDOUT}"  && shift
  usage $descriptor
  test "$#" -gt 0 && abort "$@"
  exit 0
}

# - - - - - - - - - - -
  long_usage  () {
# - - - - - - - - - - -
#
  local descriptor="${1:-STDOUT}"  && shift
  usage $descriptor
  cat >&${!descriptor} <<-EOUSAGE
${NAME}-${VERSION} README

Description
===========
${NAME} (pgf) is almost a clone of pinepg [0].  It, however,
adds the capability for specifying which user ID (or IDs) to use for
the encryption or signing.  It does absolutely no passphrase
caching.  If you need (want) passphrase caching, look into
gpg-agent, available in the "gnupg2" package, e.g., gnupg-1.9.21 as
of 2006-07-04.

This utility is intended to allow the pine user to be able to take
advantage of multiple separate roles and keys for each of those
roles within pine.  Other pine and GnuPG integration (i.e.,
pgp4pine, pinepg) utilities do not allow for selection of user ID
for a particular encryption or signing request.


Required Option(s)
==================
At least one of these first four options is required.  The sign and
encrypt options may be combined, but no other combination is
possible.  If multiple options are specified in incompatible
combinations, the last one "wins".

 --clearsign (-c)       Have GPG clearsign the message.  This is what
                        most people mean when they talk about "signed"
                        email messages.
 --encrypt (-e)         Have GPG encrypt the message.  When using
                        this option, you must specify recipient
                        addresses, as well.
 --sign (-s)            Have GPG sign the message (use with encrypt).
 --decrypt (-d)         Have GPG decrypt the message.
 --verify (-v)          Have GPG verify the message.

These options

 --to-self (-t)         This option asks GPG to employ the encrypt-to-self
                        option, where self is defined as the local
                        user <key_id> specified with the -L option.
                        (This option is simply ignored if not needed.)

 --one-line (-o)        Only print out one line of the status generated
                        by GnuPG on the STDERR (_RESULTFILE_).
 --multi-line (-m)      Produce all of the usual GnuPG status output.
 --no-comment (-n)      Suppress the default comment string.
 --comment (-C)         Override the default comment string. Default is
                        $PGF_COMMENT
 --resultfile (-R)      Pine returns the filter's STDERR via the
                        _RESULTFILE_ (cf. Examples).
 --datafile (-D)        Pine provides a session-unique filename called
                        We use it as a FIFO.  _DATAFILE_ (cf. Examples).
 --local-user (-l)      Select a specific local key identifier.  With this
                        option, you may specify a non-default key on
                        your GPG keyring.  The <key_id> may be an
                        email address or the actual key ID itself.


Prerequisites
=============
You must have a working GnuPG installation.  If you are using these
scripts for integration with your pine MUA and you have only a
single GPG identity (one secret key), you may wish to consider the
GnuPG option (check ~/.gnupg/options or ~/.gnupg/gpg.conf in newer
versions) known as "encrypt-to".  The encrypt-to option allows you
to specify a key identifier which will be added to the recipient
list for any encryption.

Although not strictly a prerequisite, you may find that the
passphrase caching offered by gpg-agent (included in SuSE's gpg2
package) will save some keystrokes when used with this filter, which
performs none of its own caching.


Notes on sending-filters and display-filters
============================================

all scripts:  All of the scripts can make use of the _RESULTFILE_
  token as either a sending-filter or a display-filter.  The
  _RESULTFILE_ is used to store output like "gpg: Good signature..."
  while the filter itself is running.

  I recommend invoking each of the utilities with the "--resultfile
  _RESULTFILE_" option (see Examples, below).  Pine will pause and
  display the STDERR output from GPG, which has ended up in the
  _RESULTFILE_.  This option is, perhaps, less important when used
  on the sending-filters.

  If you are absolutely sure you want to bypass the confirmations
  of successful decryption or verification, you may omit the "-R
  _RESULTFILE_" (in which case, the output will go to the terminal
  "behind" pine's curses interface) or you may specify "-R
  /dev/null" to obliterate the output.

  I would encourage the use of "--resultfile _RESULTFILE_", because
  the user will then receive feedback from the GPG regarding the
  signature (or encryption).

pgf-verify:  The pgf-verify command requires the use of a temporary
  file (it uses a FIFO) in order to both output the text of the
  message and validate the signature.
  
pgf-encrypt, pgf-sign-encrypt:  Any use of the encryption command
  requires a recipient.  If there is no recipient, then the command
  will fail.  Pine will provide the recipient list, including Cc and
  Bcc (careful!), to the filters as command line arguments.  By
  using the _RECIPIENTS_ token in the sending-filter, you are asking
  pine to provide these addresses.


Simplest usage
==============

With the exception of the incorrect path, the below examples are the
minimum configuration set for using the ${NAME} tools.

display-filters=_LEADING("-----BEGIN PGP MESSAGE-----")_ /path/to/pgf-decrypt -R _RESULTFILE_,
	_LEADING("-----BEGIN PGP SIGNED MESSAGE-----")_ /path/to/pgf-verify -D _DATAFILE_ -R _RESULTFILE_

sending-filters=/path/to/pgf-clearsign -R _RESULTFILE_
	/path/to/pgf-sign-encrypt -R _RESULTFILE_ _RECIPIENTS_


Examples
========
Here are several examples of how to configure your pine filters:

  
0: /path/to/pgf-sign-encrypt --resultfile _RESULTFILE_ _RECIPIENTS_
0: /path/to/pgf-sign-encrypt -r _RESULTFILE_ _RECIPIENTS_

   The above will select the primary GPG key ID to use for signing
   and encrypting the message.  These two are exactly the same, one
   uses the long command line options, and the other uses the short
   command line options.

1: /path/to/pgf-sign-encrypt -tl 0x2486302b -R _RESULTFILE_ _RECIPIENTS_
1: /path/to/pgf-sign-encrypt --to-self --local-user 0x2486302b \
     --resultfile _RESULTFILE_ _RECIPIENTS_

   In these examples, we are specifying the key identifier
   (0x0x2486302b) we wish to use when encrypting.  The --to-self
   (-t) option instructs GPG to encrypt the message not only to the
   _RECIPIENTS_, but also to the key 0x2486302b.

   Because there may be multiple _RECIPIENTS_, this should be the
   last parameter on the command line.

2: /path/to/pgf-clearsign -l frank@cheese-shop.net -R _RESULTFILE_
2: /path/to/pgf-clearsign --local-user frank@cheese-shop.net \
     --resultfile _RESULTFILE_

   In this case, we are asking GPG to use the key for address
   frank@cheese-shop.net.

3: /path/to/pgf-clearsign -C "Organization Name" -R _RESULTFILE_

   We do not specify a key ID here, so the default GPG user will be
   selected.  The --comment (-C) option allows you to alter the
   string sent in the body of the GPG signature (in this case, or
   the header of the encrypted message in the case of encryption).

4: /path/to/pgf-sign-encrypt -nR _RESULTFILE_ _RECIPIENTS_

   Maybe you don't like the advertisement for pine-gpg-filter in the
   clearsign text of your messages.  Suppress the comment string
   by using the --no-comment (-n) option.


Future
======
There is a not-yet-implemented feature called "--autoid".  By taking
advantage of pine's _INCLUDEALLHDRS_ directive, we should be able to
grab the address between the wedges '<>' on the From: line and use
that email address as the GPG ID.


Similar applications
====================

 [0] http://quantumlab.net/pine_privacy_guard/     pinepg
 [1] http://hany.sk/~hany/software/pinepgp/        pinepgp
 [2] http://user.cs.tu-berlin.de/~gator/pgp4pine/  PGP4Pine (PAPP) 
 [3] http://dougbarton.net/FreeBSD/Downloads/      Pine PGP Filters 
 [4] http://pgpenvelope.sourceforge.net/           pgpenvelope

EOUSAGE

exit 0

}

# - - - - - - - - - - -
  parse_options () {
# - - - - - - - - - - -
#
  command getopt                    \
    --unquoted                      \
    --name "${0##*/}"               \
    --options "${OPTIONS}"          \
    --longoptions "${LONGOPTIONS}"  \
    -- "$@"
}

# - - - - - - - - - - -
  verify_support  () {
# - - - - - - - - - - -
#
# -- all we want to do is make sure that all of our support
# programs exist.
#
  local program
  local programs="$@"
  local N
  local ok=0           # -- return 0, if everything is OK!
  
  for program in $programs; do
    N=$( which "$program" 2>/dev/null )
    if test -z "$N"; then
      ok=1
      gripe STDERR "Could not locate necessary support program:  $program"
    fi
  done
  return $ok
}

# - - - - - - - - - - -
  one_line_check  () {
# - - - - - - - - - - -
#
# -- test to see if the user wants (only) one-line status messages
#
  test -z "$PGF_RESULTFILE"                 && return
  test    "$PGF_RESULTFILE" = "/dev/null"   && return
  test    "$PGF_RESULTFILE" = "/dev/stderr" && return
  test    "$PGF_STATLINES"  = "multi"       && return

  LINE="$( tail -1 $PGF_RESULTFILE )"

  printf "%s\n" "$LINE" > "$PGF_RESULTFILE"

}

# - - - - - - - - - - -
  pgf_catchall () {
# - - - - - - - - - - -
#
# -- call the GnuPG utility for any general purpose
#
  gpg $GPG_OPT_ACTION "$PGF_COMMENT" $GPG_OPT_LU

  EC=$?

  one_line_check

  exit $EC

}

# - - - - - - - - - - -
  pgf_encrypt  () {
# - - - - - - - - - - -
#
# -- call the GnuPG utility to encrypt (and maybe sign) the message on STDIN
#

  # -- we have to have recipients, or else there's no work to do,
  #    and this has to be an error.
  #
  test -n "$PGF_RECIPIENTS" \
    || abort "No recipient address(es) specified."
  
  gpg $GPG_OPT_ACTION "$PGF_COMMENT" $GPG_OPT_LU $GPG_OPT_RECIPS ;

  EC=$?

  one_line_check

  exit $EC

}

# - - - - - - - - - - -
  pgf_verify  () {
# - - - - - - - - - - -
#
# -- call the GnuPG utility to verify the message on STDIN
#
  rm -f "$PGF_FIFO" \
    || abort "Could not remove $PGF_FIFO (_DATAFILE_) prior to mkfifo."

  mkfifo --mode 0700 "$PGF_FIFO" \
    || abort "Could not create FIFO $PGF_FIFO for verify option."

  # -- very ugly; have to put gpg in the background, reading
  #    from a FIFO while we use "tee" to drop the data into
  #    the FIFO and spit to STDOUT, for the user to read the
  #    message.
  #
  gpg $GPG_OPT_ACTION "$PGF_COMMENT" $GPG_OPT_LU < "$PGF_FIFO" &

  GPG_PID=$!

  tee "$PGF_FIFO"

  wait $GPG_PID

  EC=$?

  one_line_check

  exit $EC

}

# -- check to make sure that we have a few key utilities before
#    launching off the command line processing
#
verify_support which \
  || abort "${0##*/}: Cannot proceed without \"which\" utility, quitting."

verify_support getopt \
  || abort "${0##*/}: Cannot process command line options, quitting."

# - - - - - - - - - - -
# main () {
# - - - - - - - - - - -
#
# -- normalize the command-line options
#
OPTIONS="vqhLVcesdytmonC:R:D:l:a::"
LONGOPTIONS="verbose,quiet,help,long-usage,version"
LONGOPTIONS="$LONGOPTIONS,clearsign,encrypt,sign,decrypt,verify,to-self"
LONGOPTIONS="$LONGOPTIONS,no-comment,comment:,datafile:,resultfile:"
LONGOPTIONS="$LONGOPTIONS,multi-line,one-line,local-user:,autoid::"

# -- calling parse_options twice seems silly.  It is.  It allows, us
#    however, to control the error reporting to the user a bit more
#    carefully.  All the OCD kids are doing it.
#
parse_options "$@" >/dev/null \
  || abort "Try \"${0##*/} --help\" for more information."

set -- $( parse_options "$@" )

# -- Bump up verbosity a notch if we are connected to a terminal;
#    generally, users like to know a bit more of what's going on...
#
tty --silent         && let VERBOSE=VERBOSE+1

# -- examples of options/arguments should look like this now:
# 
while test "$#" -gt "0" ; do
   case "$1" in

      -h | --h*    ) short_usage                              ;;
      -L | --long* ) long_usage                               ;;
      -V | --vers* ) version                                  ;;
      -v | --verb* ) let VERBOSE=VERBOSE+1                    ;;
      -q | --q*    ) VERBOSE=0 ; PGF_QUIET="--quiet"          ;;

      -s | --s*    ) PGF_MODE="S ${PGF_MODE}"                 ;;
      -e | --e*    ) PGF_MODE="${PGF_MODE} E"                 ;;
      -c | --clea* ) PGF_MODE="${PGF_MODE} E"                 ;;
      -d | --decr* ) PGF_MODE="D"                             ;;
      -y | --veri* ) PGF_MODE="V"                             ;;
      -a | --a*    ) PGF_AUTOID="true"      && shift          ;;
      -l | --loca* ) PGF_LOCAL_USER="$2"    && shift          ;;
      -t | --t*    ) PGF_TO_SELF="true"                       ;;

      -m | --m*    ) PGF_STATLINES="multi"  && shift          ;;
      -o | --o*    ) PGF_STATLINES="one"    && shift          ;;
      -n | --n*    ) PGF_NOCOMMENT="true"   && shift          ;;
      -C | --comm* ) PGF_COMMENT="$2"       && shift          ;;
      -D | --d*    ) PGF_FIFO="$2"          && shift          ;;
      -R | --r*    ) PGF_RESULTFILE="$2"    && shift          ;;

             --    ) shift                  && break          ;;
             -*    ) short_usage STDERR "Unknown option: $1"  ;;

   esac
   shift
done

# -- called compatibility names override any command line switches
#    (no reason for a user to call) "pfg-verify --decrypt"

case ${0##*/} in
     
  *-sign-encrypt ) PGF_MODE="S E"     ;;
  *-sign         ) PGF_MODE="S"       ;;
  *-clearsign    ) PGF_MODE="C"       ;;
  *-encrypt      ) PGF_MODE="E"       ;;
  *-decrypt      ) PGF_MODE="D"       ;;
  *-verify       ) PGF_MODE="V"       ;;
  *              ) :                  ;;  # -- not strictly required

esac

case ${0##*/} in

  pgfa-*         ) PGF_AUTOID="true"  ;;
  pgf-*          ) unset PGF_AUTOID   ;;
  *              ) :                  ;;  # -- not strictly required

esac

# -- all remaining arguments are taken to be recipient addresses
#
PGF_RECIPIENTS="$@"

verify_support $REQUIRED_UTILITIES \
  || abort "${0##*/}: Missing utilities, cannot continue, quitting."

test -n "$PGF_MODE" \
  || short_usage STDERR "One of encrypt, sign, decrypt or verify must be specified."

# -- actions taken after sanity checking

# -- iterate over the list of recipients and add them to the list
#    of addresses to which to encrypt the message
#
for RECIP in $PGF_RECIPIENTS ; do

  GPG_OPT_RECIPS="$GPG_OPT_RECIPS --recipient $RECIP"

done

# -- find out if the user specified an alternate email address and
#    specify that user ID for the following commands.  This is not necessary
#    for the --decrypt and --verify options and GnuPG-1.4.x simply ignores
#    the extraneous option.
#
if test -n "$PGF_LOCAL_USER" ; then

  GPG_OPT_LU="--local-user $PGF_LOCAL_USER"

  test -n "$PGF_TO_SELF" \
    && GPG_OPT_LU="$GPG_OPT_LU --encrypt-to $PGF_LOCAL_USER"

fi

# -- Let's see about suppressing that pretty little "comment" string,
#    shall we?  The --comment (which is completely sugar) is included
#    in the header of the PGP encrypted (or signed) data.
#   
test -n "$PGF_NOCOMMENT"  && PGF_COMMENT=""

# -- send all STDERR nonsense from the gpg binary to pine's result
#    if the argument has been specified (otherwise, we'll just leave
#    STDERR alone)
#
test      -n "$PGF_RESULTFILE" \
  && exec 2> "$PGF_RESULTFILE"

# -- build up the primary action string for passing to GnuPG
#
GPG_OPT_ACTION="$PGF_QUIET --armor"

for ACTION in $PGF_MODE ; do

  GPG_OPT_ACTION="$GPG_OPT_ACTION ${!ACTION}"

done

GPG_OPT_ACTION="$GPG_OPT_ACTION --comment"

# -- And now, we are simply going to call GPG and pass this off.
#    We rely on GnuPG's error code to propagate back to pine, since
#    we are not handling any temporary files (if we can avoid it).
#
case "$PGF_MODE" in

  *E) pgf_encrypt      ;;
   V) pgf_verify       ;;
   *) pgf_catchall     ;;

esac

# -- end of file
