]> gitweb.fluxo.info Git - firma.git/commitdiff
LOTS of changes. Check the CHANGELOG for details.
authorluis <luis>
Wed, 17 Aug 2005 00:59:17 +0000 (00:59 +0000)
committerluis <luis>
Wed, 17 Aug 2005 00:59:17 +0000 (00:59 +0000)
firma

diff --git a/firma b/firma
index 523695d07a0f8c90e97478845a97b0ce438622b3..1f0ff8ae665031d544a8f11e4402b53e758279df 100755 (executable)
--- a/firma
+++ b/firma
@@ -1,35 +1,93 @@
 #!/bin/bash
 #
-# firma: encrypted mailing list manager
-# feedback: rhatto@riseup.net luis@riseup.net | GPL
+# firma: GnuPG-based encrypted mailing list manager
+# Feedback: rhatto@riseup.net, luis@riseup.net
+# Licensed under the GNU Public License.
 #
-# list configuration is passed thru the config file,
-# where you put PARAMETER=value (whithout spaces)
+# All firma parameters are passed through two different
+# configuration files: firma.conf, containing general parameters
+# necessary to run the script, and a list specific file,
+# containing its address, administrator(s), etc. In both
+# files you should enter PARAMETER='value' (whithout spaces
+# before or after the equal sign).
 #
-# MAIL= path for mail program
-# MAIL_ARGS= command-line arguments passed to the smtp wrapper
-# GPG= path for gnupg binary
-# LISTNAME= list email
-# LISTADMIN= list administrator email addresses (space separated)
-# GPGDIR= gpg dir for the lists' keyring
-# PASSWD= passwd for the lists' keyring
+# firma.conf should contain the following parameters:
+#
+# GPG_BINARY= path to the gnupg binary
+# MAIL_AGENT= path to the mail transport agent to be used (e.g., sendmail)
+# MAIL_AGENT_ARGS= command-line arguments to be passed to the command above
+# LISTS_DIR= path to the mailing lists directory
+#
+# And the list configuration file should contain:
+#
+# LIST_ADDRESS= list's email address
+# LIST_ADMIN= list's administrators email addresses (space separated)
+# LIST_HOMEDIR= list's gnupg homedir, where the list's keyrings are located
+# PASSPHRASE= passphrase for the list's private keyring
+#
+# NOTE: The PASSPHRASE value _has_ to be enclosed in single
+# quotes and _cannot_ contain any additional single quote as part
+# of itself.
 #
 
-FIRMA_LIST_PATH="/usr/local/etc/lists"
+FIRMA_CONFIG_FILE="/usr/local/etc/firma.conf"
 VERSION="0.3"
 
-function usage {
-  echo "usage: $(basename $0) OPTION [LIST-NAME]"
-  echo "  -a: admin commands"
-  echo "  -c: create a new list"
-  echo "  -h: display this help and exit"
-  echo "  -p: process a message"
-  echo "  -r: admin and user requests (mail only)"
-  echo "  -v: output version information and exit"
+function DeclareGpgVars {
+  #-------------------------------------------------------------
+  # declare gpg global variables
+  #
+  # parameter(s): none
+  # depends on function(s): none
+  # returns: 0
+  #-------------------------------------------------------------
+  GPG_FLAGS="--no-options --no-default-keyring --homedir $LIST_HOMEDIR --quiet --batch --no-tty --no-use-agent --no-auto-check-trustdb --no-permission-warning"
+  GPG="$GPG_BINARY $GPG_FLAGS"
+  GPG_LIST_KEYS="$GPG --list-keys --with-colons"
+  GPG_DECRYPT="$GPG --passphrase-fd 0 --decrypt"
+  GPG_ENCRYPT="$GPG --armor --trust-model always --local-user $LIST_ADDRESS --no-emit-version --passphrase-fd 0 --sign --encrypt --recipient"
+}
+
+
+function Usage {
+  #-------------------------------------------------------------
+  # display help and exit
+  #
+  # parameter(s): none
+  # depends on function(s): none
+  # returns: 0
+  #-------------------------------------------------------------
+  echo "Usage: $(basename $0) OPTION [LIST-NAME]"
+  echo "GnuPG-based encrypted mailing list manager."
+  echo
+  echo "  -a, --admin-task LIST-NAME        process administrative tasks on list"
+  echo "  -c, --create-newlist LIST-NAME    create a new mailing list"
+  echo "  -h, --help                        display this help and exit"
+  echo "  -p, --process-message LIST-NAME   process a message sent to list"
+#  echo "  -r, --process-request LIST-NAME   process administrative and user"
+#  echo "                                      requests on list."
+  echo "  -v, --version                     output version information and exit"
+  echo
+  echo "If option -a is given, read standard input for tasks to be performed."
+  echo "Tasks can be one or more of the following:"
+  echo
+  echo "  use EMAIL-ADDRESS   use the given address for message delivery instead"
+  echo "                        of the primary address on key"
+  echo
+#  echo "Report bugs to <email@goes.here>"
 }
 
-function version {
+
+function Version {
+  #-------------------------------------------------------------
+  # output version information and exit
+  #
+  # parameter(s): none
+  # depends on function(s): none
+  # returns: 0
+  #-------------------------------------------------------------
   echo "firma $VERSION"
+  echo
   echo "Copyright (C) 2005 A Firma, Inc."
   echo "This program comes with ABSOLUTELY NO WARRANTY."
   echo "This is free software, and you are welcome to redistribute it"
@@ -37,391 +95,679 @@ function version {
   echo "for more details."
 }
 
-function check_config {
-  # check configuration file parameters
-  if [ ! -f $GPG -o ! -x $GPG ]; then
-    echo "$CONFIG_FILE: GPG binary ($GPG) could not be found."
+
+function CheckFirmaConfigFile {
+  #-------------------------------------------------------------
+  # check firma.conf parameters
+  #
+  # parameter(s): none
+  # depends on function(s): none
+  # returns: 0 if all checks are passed, 1 if any check fails
+  #-------------------------------------------------------------
+  if [[ ! -f "$GPG_BINARY" || ! -x "$GPG_BINARY" ]]; then
+    echo "$(basename $0): GPG binary ("$GPG_BINARY") could not be found."
+    exit 1
+  elif [[ ! -f "$MAIL_AGENT" || ! -x "$MAIL_AGENT" ]]; then
+    echo "$(basename $0): Mail transport agent binary ("$MAIL_AGENT") could not be found."
     exit 1
-  elif [ ! -f $MAIL -o ! -x $MAIL ]; then
-    echo "$CONFIG_FILE: Mail program ($MAIL) could not be found."
+  elif [[ ! -d "$LISTS_DIR" ]]; then
+    echo "$(basename $0): Lists directory ("$LISTS_DIR") could not be found."
     exit 1
-  elif [ ! -d $GPGDIR -o ! -f $GPGDIR/pubring.gpg -o ! -f $GPGDIR/secring.gpg ]; then
-    echo "$CONFIG_FILE: GPG home directory ($GPGDIR) or the GPG keyrings could not be found."
+  fi
+}
+
+
+function CheckListConfigFile {
+  #-------------------------------------------------------------
+  # check configuration file parameters
+  #
+  # parameter(s): none
+  # depends on function(s): DeclareGpgVars
+  # returns: 0 if all checks are passed, 1 if any check fails
+  #-------------------------------------------------------------
+
+  local administrator
+
+  if [[ ! -d "$LIST_HOMEDIR" || ! -f "$LIST_HOMEDIR/pubring.gpg" || ! -f "$LIST_HOMEDIR/secring.gpg" ]]; then
+    echo "$LIST_NAME: GPG home directory ("$LIST_HOMEDIR") or the GPG keyrings could not be found."
     exit 1
-  elif [ -z "$(cat $CONFIG | grep -o ^PASSWD=\'[^\']*\'$)" -o \
-         -z "$(echo -n $PASSWD)" -o \
-         "$(echo -n $PASSWD | wc -m)" -lt "25" -o \
-         -z "$(echo -n $PASSWD | grep -o [[:lower:][:upper:]])" -o \
-         -z "$(echo -n $PASSWD | grep -o [[:digit:]])" -o \
-         "$(echo -n $PASSWD | grep -o [[:punct:]] | wc -l)" -lt "5" ]; then
-    echo "$CONFIG_FILE: PASSWD is empty or does not meet the minimum complexity requirements."
-    echo "$CONFIG_FILE: Please set a new passphrase for the list's private key. Make it at least"
-    echo "$CONFIG_FILE: 25 characters long (using a combination of letters, numbers and at least"
-    echo "$CONFIG_FILE: 5 special characters) and enclose it in 'single quotes'. The passphrase"
-    echo "$CONFIG_FILE: itself, though, cannot contain any single quote."
+  elif [[ -z "$(cat $LIST_CONFIG_FILE | grep -o "^PASSPHRASE='[^']*'$")" || \
+         -z "$PASSPHRASE" || \
+         "$(echo -n "$PASSPHRASE" | wc -c)" -lt "25" || \
+         -z "$(echo -n "$PASSPHRASE" | tr -dc '[[:lower:]]')" || \
+         -z "$(echo -n "$PASSPHRASE" | tr -dc '[[:upper:]]')" || \
+         -z "$(echo -n "$PASSPHRASE" | tr -dc '[[:digit:]]')" || \
+         "$(echo -n "$PASSPHRASE" | tr -dc '[:punct:]' | wc -c)" -lt "5" || \
+         "$(echo -n "$PASSPHRASE" | fold -w1 | uniq -cd | grep -v '^ \{6\}[23] ')" ]]; then
+    echo "$LIST_NAME: PASSPHRASE is empty or does not meet the minimum complexity requirements."
+    echo "$LIST_NAME: Please set a new passphrase for the list's private key. Make it at least"
+    echo "$LIST_NAME: 25 characters long (using a combination of numbers, upper and lower case"
+    echo "$LIST_NAME: letters and at least 5 special characters) and enclose it in 'single"
+    echo "$LIST_NAME: quotes'. The passphrase itself, though, cannot contain any single quote."
+    echo "$LIST_NAME: Also, no character should be sequentially repeated more than 3 times."
     exit 1
-  elif [ -z "$($GPGLIST | grep ^pub | cut -d : -f 10 | grep -i \<$LISTNAME\>$)" ]; then
-    echo "$CONFIG_FILE: GPG key for list \"$LISTNAME\" could not be found."
-    echo "$CONFIG_FILE: Note that this parameter expects an email address."
+  elif [[ -z "$($GPG_LIST_KEYS --fixed-list-mode 2> /dev/null | grep ^uid | cut -d : -f 10 | grep -i "<$LIST_ADDRESS>$")" ]]; then
+    echo "$LIST_NAME: Public key for list \"$(echo -ne "$LIST_ADDRESS" | tr '[:upper:]' '[:lower:]')\" could not be found."
+    echo "$LIST_NAME: Note that this parameter expects an email address."
     exit 1
   else
-    for ADMIN in $LISTADMIN; do {
-      if [ -z "$($GPGLIST | grep ^pub | cut -d : -f 10 | grep -i \<$ADMIN\>$)" ]; then
-        echo "$CONFIG_FILE: GPG key for list administrator \"$ADMIN\" could not be found."
-        echo "$CONFIG_FILE: Note that this parameter expects one or more space separated email addresses."
+    for administrator in $LIST_ADMIN; do {
+      if [[ -z "$($GPG_LIST_KEYS --fixed-list-mode 2> /dev/null | grep ^uid | cut -d : -f 10 | grep -i "<$administrator>$")" ]]; then
+        echo "$LIST_NAME: Public key for list administrator \"$(echo -ne "$administrator" | tr '[:upper:]' '[:lower:]')\" could not be found."
+        echo "$LIST_NAME: Note that this parameter expects one or more space separated email addresses."
         exit 1
       fi; }
     done
   fi
 }
 
-function get_gpg_stderr {
-  # discard $GPGDECRYPT STDOUT and get its STDERR instead, for signature checking
-  echo -e "$PASSWD\n${GPG_MESSAGE[@]}" | sed -e 's/^ //' | ($GPGDECRYPT --status-fd 2 1> /dev/null) 2>&1
-}
 
-function get_subscribers_list {
-  # get list susbscriber's addresses
-  $GPGLIST | sed -ne "/$LISTNAME/Id" -e '/pub/p' | cut -d : -f 10 | grep -o '<[^<>]*>$' | sed -e 's/[<>]//g'
-}
+function GetMessage {
+  #-------------------------------------------------------------
+  # read message from STDIN
+  #
+  # parameter(s): expects message from STDIN
+  # depends on function(s): none
+  # returns: 0 on success, 1 if there's no input
+  #-------------------------------------------------------------
+
+  local element
 
-function get_message {
-  LINES=0
+  # store message in array ORIG_MESSAGE
   while read STDIN; do
-    MESSAGE[$LINES]="$STDIN\n"
-    ((++LINES))
+    ORIG_MESSAGE[$element]="$STDIN\n"
+    ((++element))
   done
+
+  # check if message was successfully stored in ORIG_MESSAGE
+  if [[ "${#ORIG_MESSAGE[@]}" -eq "0" ]]; then
+    echo "$(basename $0): Message couldn't be read from standard input."
+    exit 1
+  fi
 }
 
-function get_gpg_message {
-  signal=0
-  if [ ! -z "$LINES" ]; then
-    n=0;
-    for ((count=0;count<=LINES;count++)); do
-      if [[ $signal == "0" ]] && [[ "$(echo "${MESSAGE[$count]}" | grep -v -e "-----BEGIN PGP MESSAGE-----")" == "" ]]; then
-        GPG_MESSAGE[$n]=${MESSAGE[$count]}
-        ((++n))
-        signal=1
-      elif [[ $signal == "1" ]]; then
-        GPG_MESSAGE[$n]=${MESSAGE[$count]}
-        ((++n))
-        if [[ "$(echo "${MESSAGE[$count]}" | grep -v -e "-----END PGP MESSAGE-----")" == "" ]]; then
-          signal=0
-        fi
-      fi
+
+function GetMessageHeaders {
+  #-------------------------------------------------------------
+  # get message headers and store some of them on separate variables
+  #
+  # parameter(s): none
+  # depends on function(s): GetMessage
+  # returns: 0 on success, 1 if headers can't be located within message
+  #-------------------------------------------------------------
+
+  local element
+
+  # store all headers in array ORIG_MESSAGE_HEADERS
+  for element in $(seq 0 $((${#ORIG_MESSAGE[@]} - 1))); do
+    until [[ "${ORIG_MESSAGE[$element]}" == "\n" ]]; do
+      ORIG_MESSAGE_HEADERS[$element]="${ORIG_MESSAGE[$element]}"
+      ((++element))
     done
+    # done, reached first blank line in message
+    # exit for loop
+    break 1
+  done
+
+  # list ORIG_MESSAGE_HEADERS and get some specific headers for later use
+  FROM=$(echo -e "${ORIG_MESSAGE_HEADERS[@]}" | grep -m 1 '^ From:' | cut -d : -f 2- | sed -e 's/^ //')
+  SENDER_ADDRESS=$(if [[ -z "$(echo $FROM | grep '>$')" ]]; then echo $FROM; else echo $FROM | grep -o '<[^<>]*>$' | sed -e 's/[<>]//g'; fi)
+  DATE=$(echo -e "${ORIG_MESSAGE_HEADERS[@]}" | grep -m 1 '^ Date:' | sed -e 's/^ //')
+  SUBJECT=$(echo -e "${ORIG_MESSAGE_HEADERS[@]}" | grep -m 1 '^ Subject:' | cut -d : -f 2- | sed -e 's/^ //')
+
+  # check if message headers were successfully stored in ORIG_MESSAGE_HEADERS
+  if [[ "${#ORIG_MESSAGE_HEADERS[@]}" -eq "0" ]]; then
+    echo "$(basename $0): Message headers could not be located within this message."
+    return 1
   fi
 }
 
-function get_message_headers {
-  # get the message headers and the sender's email address
-  FROM=$(echo -e "${MESSAGE[@]}" | grep -m 1 "From:" | cut -d : -f 2- | sed -e 's/^ //')
-  FROMADD=$(
-    if [ -z "$(echo $FROM | grep '>$')" ]; then
-      echo $FROM
-    else
-      echo $FROM | grep -o '<[^<>]*>$' | sed -e 's/[<>]//g'
+
+function GetGpgMessage {
+  #-------------------------------------------------------------
+  # get gpg encrypted part of a message
+  #
+  # parameter(s): none
+  # depends on function(s): GetMessage
+  # returns: 0 on success, 1 if no encrypted data is found within message
+  #-------------------------------------------------------------
+
+  # i = ORIG_MESSAGE element being processed
+  # j = ORIG_GPG_MESSAGE element being processed
+  local i j
+
+  # for elements in ORIG_MESSAGE, do
+  for i in $(seq 0 $((${#ORIG_MESSAGE[@]} - 1))); do
+
+    # find the first line of the encrypted data
+    #+and assign it to the first element of ORIG_GPG_MESSAGE
+    if [[ "${ORIG_MESSAGE[$i]}" == "-----BEGIN PGP MESSAGE-----\n" ]]; then
+      ORIG_GPG_MESSAGE[$j]="${ORIG_MESSAGE[$i]}"
+
+      # move to next element in both arrays
+      ((++i))
+      ((++j))
+
+      # until the end of the encrypted data is reached,
+      #+assign subsequent elements in ORIG_MESSAGE to elements of ORIG_GPG_MESSAGE
+      until [[ "${ORIG_MESSAGE[$i]}" == "-----END PGP MESSAGE-----\n" ]]; do
+        ORIG_GPG_MESSAGE[$j]="${ORIG_MESSAGE[$i]}"
+        ((++i))
+        ((++j))
+      done
+
+      # last, assign the line matched above to the last element of ORIG_GPG_MESSAGE
+      ORIG_GPG_MESSAGE[$j]="${ORIG_MESSAGE[$i]}"
+      # no need to process lines beyond this point
+      # exit for loop
+      break 1
     fi
-  )
-  DATE=$(echo -e "${MESSAGE[@]}" | grep -m 1 "Date:" | sed -e 's/^ //')
-  SUBJECT=$(echo -e "${MESSAGE[@]}" | grep -m 1 "Subject:" | cut -d : -f 2- | sed -e 's/^ //')
+  done
+
+  # check if encrypted data was stored in ORIG_GPG_MESSAGE
+  if [[ "${#ORIG_GPG_MESSAGE[@]}" -eq "0" ]]; then
+    echo "$(basename $0): No GPG encrypted data was found within this message."
+    return 1
+  fi
+}
+
+
+function GetGpgDecryptStderr {
+  #-------------------------------------------------------------
+  # discard $GPG_DECRYPT STDOUT and get its STDERR for signature checking
+  #
+  # parameter(s): none
+  # depends on function(s): DeclareGpgVars, GetGpgMessage
+  # returns: 0 if signature in ORIG_GPG_MESSAGE is valid,
+  #          1 if signature is invalid,
+  #          2 for all other errors (incorrect passphrase, no encrypted data, etc.)
+  #-------------------------------------------------------------
+
+  echo -e "$PASSPHRASE\n${ORIG_GPG_MESSAGE[@]}" | sed -e 's/^ //' | ($GPG_DECRYPT --status-fd 2 1> /dev/null) 2>&1
+
 }
 
-function message_list {
-# compose and send a message to the list
-# $1: subscriber email
-# sorry no identation :P  
 
-LIST_MESSAGE=( $(echo "$PASSWD
+function GetSubscribersList {
+  #-------------------------------------------------------------
+  # get list susbscriber addresses for message encryption and delivery
+  #
+  # parameter(s): none
+  # depends on function(s): DeclareGpgVars
+  # returns: 0 on success, 1 if there are no subscribers on list
+  #-------------------------------------------------------------
+
+  if [[ "$($GPG_LIST_KEYS 2> /dev/null | sed -ne "/$LIST_ADDRESS/Id" -e '/^pub/p' | wc -l)" -ne "0" ]]; then
+    $GPG_LIST_KEYS 2> /dev/null | sed -ne "/$LIST_ADDRESS/Id" -e '/^pub/p' | cut -d : -f 10 | grep -o '<[^<>]*>$' | sed -e 's/[<>]//g'
+  else
+    echo "$LIST_NAME: There are no subscribers on list \"$(echo "$LIST_ADDRESS" | tr '[:upper:]' '[:lower:]')\"."
+    exit 1
+  fi
+}
+
+
+function SendListMessage {
+  #-------------------------------------------------------------
+  # compose and send a message to list members
+  #
+  # parameter(s): subscriber address
+  # depends on function(s): DeclareGpgVars, GetGpgMessage, GetMessageHeaders, GetGpgDecryptStderr
+  # returns: 0 on success
+  #-------------------------------------------------------------
+
+  # this is the body of the message to be sent, so no identation here
+MESSAGE_BODY=( $(echo "$PASSPHRASE
 Message from: $FROM
 Subject: $SUBJECT
 $DATE
 
-$(get_gpg_stderr | grep -F 'gpg: Signature made')
-$(get_gpg_stderr | grep -F 'gpg: Good signature from')
+$(GetGpgDecryptStderr | grep '^gpg: Signature made')
+$(GetGpgDecryptStderr | grep '^gpg: Good signature from')
 
-$(echo -e "$PASSWD\n${GPG_MESSAGE[@]}" | $GPGDECRYPT 2> /dev/null)" | sed -e 's/=20$//' | $GPGENCRYPT $1 | sed -e 's/^\(.*\)$/\1\\n/') )
+$(echo -e "$PASSPHRASE\n${ORIG_GPG_MESSAGE[@]}" | $GPG_DECRYPT 2> /dev/null)" | sed -e 's/=20$//' | $GPG_ENCRYPT $1 2> /dev/null | sed -e 's/$/\\n/') )
 
-# now send the message
-echo -e "From: $LISTNAME\nTo: $1\nSubject: none\n\n${LIST_MESSAGE[@]}" | sed -e 's/^ //' | $MAIL $MAIL_ARGS
-  
+  # now send the message
+  echo -e "From: $LIST_ADDRESS\nTo: $1\nSubject: none\n\n${MESSAGE_BODY[@]}" | sed -e 's/^ //' | $MAIL_AGENT $MAIL_AGENT_ARGS
 }
 
-function message_list_error {
-# compose and send an error message
-# sorry no identation :P  
 
-LIST_MESSAGE=( $(echo "$PASSWD
+function SendWarningMessage {
+  #-------------------------------------------------------------
+  # compose and send a "BAD signature" warning to the
+  # list administrator(s) and to sender
+  #
+  # parameter(s): list administrator/sender address
+  # depends on function(s): DeclareGpgVars, GetGpgMessage, GetMessageHeaders, GetGpgDecryptStderr
+  # returns: 0 on success
+  #-------------------------------------------------------------
+
+  # this is the body of the message to be sent, so no identation here
+MESSAGE_BODY=( $(echo "$PASSPHRASE
 Message from: $FROM
 Subject: [BAD SIGNATURE] $SUBJECT
 $DATE
-  
-$(get_gpg_stderr | grep -F 'gpg: Signature made')
-$(get_gpg_stderr | grep -F 'gpg: BAD signature from')
-$(echo -e "$PASSWD\n${GPG_MESSAGE[@]}" | $GPGDECRYPT 2> /dev/null)" | sed -e 's/=20$//' | $GPGENCRYPT $1 | sed -e 's/^\(.*\)$/\1\\n/') )
 
-# now send the message
-echo -e "From: $LISTNAME\nTo: $1\nSubject: none\n\n${LIST_MESSAGE[@]}" | sed -e 's/^ //' | $MAIL $MAIL_ARGS
+$(GetGpgDecryptStderr | grep '^gpg: Signature made')
+$(GetGpgDecryptStderr | grep '^gpg: BAD signature from')
+
+$(echo -e "$PASSPHRASE\n${ORIG_GPG_MESSAGE[@]}" | $GPG_DECRYPT 2> /dev/null)" | sed -e 's/=20$//' | $GPG_ENCRYPT $1 2> /dev/null | sed -e 's/$/\\n/') )
 
+  # now send the message
+  echo -e "From: $LIST_ADDRESS\nTo: $1\nSubject: none\n\n${MESSAGE_BODY[@]}" | sed -e 's/^ //' | $MAIL_AGENT $MAIL_AGENT_ARGS
 }
 
-function message_list_return {
-# send a bounce message
-# $1: sender email (usually $FROMADD)
-# sorry no identation :P  
-echo "From: $LISTNAME
+
+function SendBounceMessage {
+  #-------------------------------------------------------------
+  # send a bounce message back to sender
+  #
+  # parameter(s): sender address
+  # depends on function(s): GetMessageHeaders
+  # returns: 0 on success
+  #-------------------------------------------------------------
+
+  # this is the body of the message to be sent, so no identation here
+echo "From: $LIST_ADDRESS
 To: $1
 Subject: none
 
 Message from: $FROM
 Subject: [RETURNED MAIL] $SUBJECT
 $DATE
-  
 [ It was not possible to process this message. Either or both
   the message was not encrypted and/or signed, or you are not
   subscribed to this list.  Contact the list administrator if
-    you have any questions. ]
-  -- 
-  firma v$VERSION" | $MAIL $MAIL_ARGS 
+
+ It was not possible to process this message. Either or both
+ the message was not encrypted and/or signed, or you are not
+ subscribed to this list.  Contact the list administrator if
+ you have any questions.
+
+-- 
+firma v$VERSION" | $MAIL_AGENT $MAIL_AGENT_ARGS
 }
 
-function process_message {
-  # process a message sent to the list
 
-  get_message
-  get_message_headers  
-  get_gpg_message
+function ProcessMessage {
+  #-------------------------------------------------------------
+  # process a received message
+  #
+  # parameter(s): none
+  # depends on function(s): GetMessage, GetMessageHeaders, GetGpgMessage, GetGpgDecryptStderr,
+  #                         GetSubscribersList, SendListMessage, SendWarningMessage, SendBounceMessage
+  # returns: 0 on success
+  #-------------------------------------------------------------
 
-  # if signature is Good, encrypt and send it for each list subscriber
-  # todo: declare a function to decrypt, re-encrypt and send the list messages
-  if (get_gpg_stderr | grep -Fq GOODSIG); then
-  
-    for EMAIL in $(get_subscribers_list); do 
-      message_list $EMAIL
+  local email
+
+  GetMessage
+  GetMessageHeaders
+  GetGpgMessage
+
+  # if signature in message is valid, encrypt and send it for each list subscriber
+  if GetGpgDecryptStderr | grep -q '^\[GNUPG:] GOODSIG'; then
+    for email in $(GetSubscribersList); do
+      SendListMessage $email
     done
-  
-  # else, if signature is BAD, email it back to the list admins and to sender
-  elif (get_gpg_stderr | grep -Fq BADSIG) ; then
-  
-    for EMAIL in $(echo $LISTADMIN $FROMADD); do
-      message_list_error $EMAIL
+
+  # else, if signature is invalid, email it back to the list administrator(s) and to sender
+  elif GetGpgDecryptStderr | grep -q '^\[GNUPG:] BADSIG'; then
+    for email in $LIST_ADMIN $SENDER_ADDRESS; do
+      SendWarningMessage $email
     done
-  
-  # else, probably either the message was not signed or the sender is not subscribed to the list
-  # email the message back to sender including a note about this
+
+  # else, probably either the message was not encrypted/signed or the sender is not subscribed to the list
+  # send a bounce message back to sender including a note about this
   # todo: parse STDERR to find out why the signature couldn't be checked and send more specific errors back to sender
   else
-    message_list_return $FROMADD 
+    SendBounceMessage $SENDER_ADDRESS
   fi
-   
+
 }
 
-function newlist {
-  # create a list if it doesnt already exist
-  if [ ! -d "$CONFIG_PATH" ]; then
-    echo creating folder $CONFIG_PATH...
-    mkdir "$CONFIG_PATH" # || (echo "error creating $CONFIG_PATH: installation aborted"; exit 1)
+
+function NewList {
+  #-------------------------------------------------------------
+  # create a list if it doesn't exist yet
+  #
+  # parameter(s): none
+  # depends on function(s): DeclareGpgVars
+  # returns: 0 on success, 1 if list already exists or cannot be created
+  #-------------------------------------------------------------
+
+  if [ ! -d "$LIST_PATH" ]; then
+    echo creating folder $LIST_PATH...
+    mkdir "$LIST_PATH" # || (echo "$(basename $0): error creating $LIST_PATH: installation aborted"; exit 1)
     echo "creating list config file and will ask some questions."
 
-    read -p "path to smtp command (eg, /usr/sbin/sendmail): " MAIL
-    read -p "command-line arguments passed to the smtp wrapper (eg, -oem -oi -t): " MAIL_ARGS
-    read -p "path to gpg binary (eg, /usr/bin/gpg): " GPG
+  # comented:
+#    read -rep "path to smtp command (eg, /usr/sbin/sendmail): " MAIL_AGENT
+#    read -rep "command-line arguments passed to the smtp wrapper (eg, -oem -oi -t): " MAIL_AGENT_ARGS
+#    read -rep "path to gpg binary (eg, /usr/bin/gpg): " GPG_BINARY
 
-    # if [ ! -x $GPG ]; then 
+    # if [ ! -x "$GPG_BINARY" ]; then
 
-    read -p "list keyring folder (defaults to $GPGDIR): " GPGDIR
+    read -rep "list keyring folder: " LIST_HOMEDIR # removed: (defaults to $LIST_HOMEDIR)
 
     # todo: please no utf-8 (see DETAILS)
-    read -p "list email (eg, firma@domain.tld): " LISTNAME
-    read -p "list admins emails (space delimited): " LISTADMIN
-    read -p "list description (fake?): " DESCRIPTION
-    read -p "password for list keyring (use a huge one): " PASSWD 
+    read -rep "list email (eg, firma@domain.tld): " LIST_ADDRESS
+    read -rep "list admins emails (space delimited): " LIST_ADMIN
+    read -rep "list description (fake?): " DESCRIPTION
+    read -resp "password for list keyring (use a huge one): " PASSPHRASE
 
     # todo: key specs (size, expiry date...)
 
     echo "creating your config..."
-    touch $CONFIG
-    chown root.root $CONFIG
-    chmod 600 $CONFIG
-    if [ -f $CONFIG ]; then
-      gpg_args
-      echo -e "MAIL=$MAIL\nGPG=$GPG\nGPGDIR=$GPGDIR\nLISTNAME=$LISTNAME\nLISTADMIN=$LISTADMIN\nPASSWD=$PASSWD" > $CONFIG
+    touch $LIST_CONFIG_FILE
+    chown root.root $LIST_CONFIG_FILE
+    chmod 600 $LIST_CONFIG_FILE
+    if [ -f "$LIST_CONFIG_FILE" ]; then
+      DeclareGpgVars
+      echo -e "LIST_HOMEDIR=$LIST_HOMEDIR\nLIST_ADDRESS=$LIST_ADDRESS\nLIST_ADMIN=$LIST_ADMIN\nPASSPHRASE=$PASSPHRASE" > $LIST_CONFIG_FILE # removed: MAIL_AGENT=$MAIL_AGENT\nGPG_BINARY=$GPG_BINARY\n
       echo "now generating your keyring..."
 
-      $GPGCOMMAND --gen-key <<EOF
+      $GPG --gen-key <<EOF
 
         Key-Type: DSA
         Key-Length: 1024
         Subkey-Type: ELG-E
         Subkey-Length: 1024
 
-        Name-Real: $DESCRIPTION 
-        Name-Email: $LISTNAME
+        Name-Real: $DESCRIPTION
+        Name-Email: $LIST_ADDRESS
 
         Expire-Date: 0
-        Passphrase: $PASSWD
+        Passphrase: $PASSPHRASE
         %commit
 
 EOF
 
     fi
   else
-    echo error creating $CONFIG_FILE: list already exists
-    exit 1 
+    echo "$(basename $0): cannot create $LIST_NAME: List already exists"
+    exit 1
   fi
 }
 
-function gpg_args {
-  # declare GPG variables
-  GPGFLAGS="--quiet --homedir $GPGDIR --batch --no-tty --no-use-agent --no-permission-warning"
-  GPGCOMMAND="$GPG $GPGFLAGS"
-  GPGLIST="$GPGCOMMAND --list-keys --with-colons"
-  GPGDECRYPT="$GPGCOMMAND --passphrase-fd 0 --decrypt"
-  GPGENCRYPT="$GPGCOMMAND --passphrase-fd 0 --always-trust --encrypt --sign --armor --recipient"
-}
 
-function list_admin {
-  if [ "$1" = "use" ]; then
-    if [ "$#" -lt "2" ]; then
-      echo "$(basename $0): Too few arguments. Command \"use\" expects an email address as argument."
-      return 1
-    elif [ "$#" -gt "2" ]; then
-      echo "$(basename $0): Too many arguments. Command \"use\" expects just one email address as argument."
-      return 1
-    elif [ -z "$(echo -ne $2 | grep -o '[^@]\+@[^@]\+')" ]; then
-      echo "$(basename $0): Invalid argument. Command \"use\" expects an email address as argument."
-      return 1
-    else
-      choose_uid $2
-    fi
-  else
-    echo "$(basename $0): \"$1\": command not found"
-  fi
+function ListAdministration {
+  #-------------------------------------------------------------
+  # process administrative tasks
+  #
+  # parameter(s): expects task to be performed (plus its argument(s)) from STDIN
+  # depends on function(s): ChooseUid
+  # returns: 0 if task is executed successfully,
+  #          1 if task can't be executed (command not found, too many/missing arguments, etc.)
+  #-------------------------------------------------------------
+
+  case $# in
+    1)
+      case $1 in
+        help)
+          echo
+          echo "  quit                quit this prompt"
+          echo "  help                show this help"
+          echo "  use EMAIL-ADDRESS   use the given address for message delivery instead"
+          echo "                        of the primary address on key"
+          echo
+          ;;
+        quit)
+          exit 0
+          ;;
+        use)
+          echo >&2 "$1: missing arguments (try \"help\")"
+          return 1
+          ;;
+        *)
+          echo >&2 "Command not found -- $1 (try \"help\")"
+          return 1
+          ;;
+      esac
+      ;;
+    2)
+      case $1 in
+        use)
+          # check if argument is an email address
+          if [[ -z "$(echo -ne $2 | grep -o '[^@]\+@[^@]\+')" ]]; then
+            echo >&2 "$1: invalid argument -- $2 (try \"help\")"
+            return 1
+          else
+            ChooseUid $2
+          fi
+          ;;
+        help|quit)
+          echo >&2 "$1: too many arguments -- $@ (try \"help\")"
+          return 1
+          ;;
+        *)
+          echo >&2 "Command not found -- $1 (try \"help\")"
+          return 1
+          ;;
+      esac
+      ;;
+    *)
+      case $1 in
+        help|quit|use)
+          echo >&2 "$1: too many arguments -- $@ (try \"help\")"
+          return 1
+          ;;
+        *)
+          echo >&2 "Command not found -- $1 (try \"help\")"
+          return 1
+          ;;
+      esac
+      ;;
+  esac
 }
 
-function choose_uid {
-
-  KEYID="$($GPGLIST --fixed-list-mode $1 2> /dev/null | grep ^pub | cut -d : -f 5 | grep -o '.\{8\}$')"
 
-  if [ -z "$($GPGLIST --fingerprint --fixed-list-mode $1 2> /dev/null | grep -i \<$1\>:$ | cut -d : -f 10)" ]; then
-    echo "$CONFIG_FILE: \"$(echo -ne $1 | tr A-Z a-z)\" is not associated with any public key on this keyring."
+function ChooseUid {
+  #-------------------------------------------------------------
+  # choose which UID of a public key should be used for message delivery,
+  # deleting all other UIDs on this key
+  #
+  # parameter(s): chosen email address
+  # depends on function(s): DeclareGpgVars
+  # returns: 0 on success,
+  #          1 if task can't be executed (public key not found, only one UID on key, etc.)
+  #-------------------------------------------------------------
+
+  local keyid="$($GPG_LIST_KEYS --with-fingerprint $1 2> /dev/null | grep ^fpr | cut -d : -f 10 | grep -o '.\{8\}$')"
+  local uid_count="$($GPG_LIST_KEYS --fixed-list-mode $keyid 2> /dev/null | grep ^uid | wc -l)"
+  local chosen_uid_number="$($GPG_LIST_KEYS --fixed-list-mode $keyid 2> /dev/null | grep ^uid | grep -ni $1 | cut -d : -f 1)"
+
+  # check if supplied address is associated with a public key
+  if [[ -z "$($GPG_LIST_KEYS --fixed-list-mode $1 2> /dev/null | grep -i "<$1>:$")" ]]; then
+    echo >&2 "use: \"$(echo -ne $1 | tr '[:upper:]' '[:lower:]')\" is not associated with any public key on this keyring."
     return 1
-  elif [ "$($GPGLIST --fixed-list-mode $1 2> /dev/null | grep ^uid | wc -l)" -eq "1" ]; then
-    echo "$CONFIG_FILE: \"$(echo -ne $1 | tr A-Z a-z)\" is part of the only UID on public key \"$KEYID\"."
+  # then check if there's more than one UID on this public key
+  elif [[ "$($GPG_LIST_KEYS --fixed-list-mode $1 2> /dev/null | grep ^uid | wc -l)" -eq "1" ]]; then
+    echo >&2 "use: \"$(echo -ne $1 | tr '[:upper:]' '[:lower:]')\" is part of the only UID on public key \"$keyid\"."
     return 1
-  elif [ "$($GPGLIST --fixed-list-mode $1 2> /dev/null | grep -iF $1 | cut -d : -f 10 | wc -l)" -gt "1" ]; then
-    echo "$CONFIG_FILE: \"$(echo -ne $1 | tr A-Z a-z)\" is listed in more than one UID on this keyring. Narrow down your selection"
-    echo "$CONFIG_FILE: or delete all but one of the public keys associated with this email address."
+  # and then check if there's only one public key associated with this address
+  elif [[ "$($GPG_LIST_KEYS --fixed-list-mode $1 2> /dev/null | grep -i "<$1>:$" | wc -l)" -gt "1" ]]; then
+    echo >&2 "use: \"$(echo -ne $1 | tr '[:upper:]' '[:lower:]')\" is listed in more than one UID on this keyring."
+    echo >&2 "Delete all but one of the public keys or UIDs associated with this email address."
     return 1
   fi
 
+  # if all checks are OK, run the expect script bellow
   expect -nN -- << EOF
+    # no output to STDOUT
     log_user 0
+    # set a 5 seconds timeout in case anything goes wrong
     set timeout 5
 
-    set keyid [eval exec $GPG --fingerprint --with-colons --fixed-list-mode $1 | grep ^fpr | cut -d : -f 10]
-    set uid_count [eval exec $GPG --list-keys --with-colons --fixed-list-mode \$keyid | grep ^uid | wc -l]
-    set chosen_uid [eval exec $GPG --list-keys --with-colons --fixed-list-mode \$keyid | grep ^uid | grep -ni $1 | cut -d : -f 1]
-
-    eval spawn -noecho $GPG --with-colons --command-fd 0 --status-fd 1 --edit-key \$keyid
+    # call gpg with the "--edit-key" option
+    eval spawn -noecho $GPG --with-colons --command-fd 0 --status-fd 1 --edit-key $keyid
     expect "GET_LINE keyedit.prompt" {
+      # select for deletion all UIDs other than the chosen one
       set uid 1
-      while { \$uid <= \$uid_count } {
-        if { \$uid != \$chosen_uid } {
+      while { \$uid <= $uid_count } {
+        if { \$uid != $chosen_uid_number } {
           send "uid \$uid\n"
           expect "GET_LINE keyedit.prompt"
         }
         set uid [incr uid]
       }
+      # delete selected UIDs
       send "deluid\n"
+      # confirm deletion
       expect "GET_BOOL keyedit.remove.uid.okay" {send "yes\n"}
+      # save and exit
       expect "GET_LINE keyedit.prompt" {send "save\n"}
       expect "GOT_IT"
     }
 
+    # delay until the process above terminates
     wait
-    send_user "$CONFIG_FILE: \"$(echo -ne $1 | tr A-Z a-z)\" chosen successfully. [ expr \$uid_count - 1 ] UIDs deleted from public key \"$KEYID\".\n"
-    interact
+    # send following message to user
+    send_user "use: \"$(echo -ne $1 | tr '[:upper:]' '[:lower:]')\" chosen for message delivery. [ expr $uid_count - 1 ] UID(s) deleted from public key \"$keyid\".\n"
+    exit
 
 EOF
 }
 
-# main -
-umask 0777
-export LANG=en_US
-USED_ARRAYS="MESSAGE GPG_MESSAGE LIST_MESSAGE"
-
-# declare all vars
-declare n
-for array in $USED_ARRAYS; do
-  declare -a $array
-done
+#-------------------------------------------------------------
+# main()
+#-------------------------------------------------------------
 
+# set enviromental variables and options
 export LANG=en_US
+umask 0077
 
-# command line checking
-if [ -z "$2" -a "$1" != "-c" -a "$1" != "-h" -a "$1" != "-v" ]; then
-  usage
-  exit 1
-else
-  CONFIG_FILE="$2"
-  CONFIG_PATH="$FIRMA_LIST_PATH/$2"
-  CONFIG="$CONFIG_PATH/$2.conf"
-fi
-
-# if the configuration file exists, disable "sourcepath" and evaluate the parameters
-if [ "$1" != "-c" -a "$1" != "-h" -a "$1" != "-v" ]; then
-  if [ -f $CONFIG ]; then
-    shopt -u sourcepath && source $CONFIG
-  else
-    echo "$(basename $0): Configuration file \"$CONFIG\" could not be found."
-    exit 1
-  fi
-fi
-
-# get gpg parameters and check the config
-if [ "$1" = "-a" -o "$1" = "-p" -o "$1" = "-r" ]; then
-  gpg_args
-  check_config
-fi
-
-# command line parsing
-if [ "$1" = "-a" ]; then
-
-  declare -a ADMINCOMMANDS
+# declare global arrays used during execution
+GLOBAL_ARRAYS="ORIG_MESSAGE ORIG_MESSAGE_HEADERS ORIG_GPG_MESSAGE MESSAGE_BODY"
 
-  n=0
-  while read STDIN; do
-    ADMINCOMMANDS[$n]="$STDIN\n"
-    ((n++))
-  done
+for ARRAY in $GLOBAL_ARRAYS; do
+  declare -a $ARRAY
+done
 
-  for i in $(seq 0 $((${#ADMINCOMMANDS[@]} - 1))); do
-    if [ ! -z "$(echo -ne ${ADMINCOMMANDS[$i]})" -a "$(echo -ne ${ADMINCOMMANDS[$i]} | cut -c1)" != "#" ]; then
-      list_admin $(echo -ne "${ADMINCOMMANDS[$i]}")
+# command line parsing:
+# first check number of arguments, then check what was entered
+# start main case
+case $# in
+  0)
+    echo >&2 "$(basename $0): missing arguments"
+    Usage
+    exit 1
+    ;;
+  1)
+    # start case #1
+    case $1 in
+      -h|--help)
+        Usage
+        ;;
+      -v|--version)
+        Version
+        ;;
+      # valid option called without its required argument
+      -a|--admin-task|-c|--create-newlist|-p|--process-message|-r|--list-request)
+        echo >&2 "$(basename $0): missing arguments"
+        Usage
+        exit 1
+        ;;
+      *)
+        echo >&2 "$(basename $0): invalid option -- $1"
+        Usage
+        exit 1
+        ;;
+    # end case #1
+    esac
+    ;;
+  2)
+
+    # if firma.conf exists, evaluate its parameters and check them
+    if [ -f "$FIRMA_CONFIG_FILE" ]; then
+      shopt -u sourcepath && source "$FIRMA_CONFIG_FILE"
+      CheckFirmaConfigFile
+    else
+      echo >&2 "$(basename $0): cannot source \`$FIRMA_CONFIG_FILE': No such file"
+      exit 1
     fi
-  done
 
-elif [ "$1" = "-c" ]; then
-  newlist
-elif [ "$1" = "-h" ]; then
-  usage
-elif [ "$1" = "-p" ]; then
-  process_message
-elif [ "$1" = "-r" ]; then
-  list_request
-elif [ "$1" = "-v" ]; then
-  version
-else
-  usage
-  exit 1
-fi 
-
-# un-declare all vars
-declare n
-for array in $USED_ARRAYS; do
-  unset $array
+    LIST_NAME="$2"
+    LIST_PATH="$LISTS_DIR/$LIST_NAME"
+    LIST_CONFIG_FILE="$LIST_PATH/$LIST_NAME.conf"
+
+    # start case #2
+    # branch directly to options which don't use a configuration
+    #+file or, for those which do, branch to a new case bellow
+    case $1 in
+      -c|--create-newlist)
+        NewList
+        ;;
+      -a|--admin-task|-p|--process-message|-r|--list-request)
+
+        # if the list configuration file exists, disable "sourcepath"
+        #+and evaluate list parameters
+        if [[ -f "$LIST_CONFIG_FILE" ]]; then
+          shopt -u sourcepath && source "$LIST_CONFIG_FILE"
+        else
+          echo >&2 "$(basename $0): cannot source \`$LIST_CONFIG_FILE': No such file"
+          exit 1
+        fi
+
+        # get gpg parameters and check the list configuration file
+        DeclareGpgVars
+        CheckListConfigFile
+
+        # start case #3
+        case $1 in
+          -a|--admin-task)
+
+            # read STDIN and, if line is not empty or commented, process command
+            while read -rep "Command> " STDIN; do
+              if [[ "$STDIN" && "$STDIN" != "#"* ]]; then
+                ListAdministration $STDIN
+              fi
+            done
+
+            ;;
+          -p|--process-message)
+            ProcessMessage
+            ;;
+          -r|--list-request)
+            # not implemented yet
+            #ListRequest
+            exit 0
+            ;;
+        # end case #3
+        esac
+        ;;
+      # valid option called with too many arguments
+      -h|--help|-v|--version)
+        echo >&2 "$(basename $0): too many arguments -- $@"
+        Usage
+        exit 1
+        ;;
+      *)
+        echo >&2 "$(basename $0): invalid option -- $1"
+        Usage
+        exit 1
+        ;;
+    # end case #2
+    esac
+    ;;
+  *)
+    # start case #4
+    case $1 in
+      # again, valid option called with too many arguments
+      -a|--admin-task|-c|--create-newlist|-h|--help|-p|--process-message|-r|--list-request|-v|--version)
+        echo >&2 "$(basename $0): too many arguments -- $@"
+        Usage
+        exit 1
+        ;;
+      *)
+        echo >&2 "$(basename $0): invalid option -- $1"
+        Usage
+        exit 1
+        ;;
+    # end case #4
+    esac
+    ;;
+# end main case
+esac
+
+# erase all global arrays
+for ARRAY in $GLOBAL_ARRAYS; do
+  unset $ARRAY
 done