diff options
-rwxr-xr-x | firma | 908 |
1 files changed, 627 insertions, 281 deletions
@@ -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 |