#!/bin/bash # # firma: GnuPG-based encrypted mailing list manager # Feedback: firma@sarava.org # # Firma is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later # version. # # Firma is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # A PARTICULAR PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., 59 Temple # Place - Suite 330, Boston, MA 02111-1307, USA # # Usage: # # All firma parameters are passed through two different configuration files: # firma.conf, containing general parameters needed to run the script, and a list # specific file, containing its address, administrator(s), etc. In both files # you should enter PARAMETER='value' (without spaces before or after the equal # sign). # # 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 it may contain the following optional parameters: # # USE_GPG_HIDDEN_RECIPIENT_OPTION= set to '1' to use GnuPG's --hidden-recipient # option, available from version 1.4.0 onwards # (try 'man gpg' for more information) # SEND_MESSAGES_USING_BCC= set to '1' to send list messages to all subscribers # at once using BCC (NOTE: this option can only be # used in conjunction with the option above.) # # 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 _has_ to be enclosed in single quotes and _cannot_ # contain any additional single quote as part of itself. It has to be at least # 25 characters long, combining numbers, upper and lower case letters and at # least 5 special characters. Also, no character can be sequentially repeated # more than 3 times. # FIRMA_CONFIG_FILE="/usr/local/etc/firma.conf" VERSION="0.3" 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" } 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 " } 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" echo "under certain conditions. See the GNU General Public License" echo "for more details." } 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 [[ ! -d "$LISTS_DIR" ]]; then echo "$(basename $0): Lists directory ("$LISTS_DIR") could not be found." exit 1 elif [[ "$USE_GPG_HIDDEN_RECIPIENT_OPTION" == "1" && "$($GPG_BINARY --version | head -n1 | tr -dc '[[:digit:]]')" -lt "140" ]]; then echo "$(basename $0): GPG's \"--hidden-recipient\" option is only available from version 1.4.0 onwards." exit 1 fi } function CheckListConfigFile { #------------------------------------------------------------- # check list 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 $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 "$($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 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 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 stdin local element # store message in array ORIG_MESSAGE while read stdin; do 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 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 local first_blank_line # find the first blank line in the message for element in $(seq 0 $((${#ORIG_MESSAGE[@]} - 1))); do if [[ "${ORIG_MESSAGE[$element]}" == "\n" ]]; then first_blank_line=$element # store all lines up to this one in array ORIG_MESSAGE_HEADERS for element in $(seq 0 $(($first_blank_line - 1))); do ORIG_MESSAGE_HEADERS[$element]="${ORIG_MESSAGE[$element]}" done # done, exit for loop break 1 fi done # 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." exit 1 fi # 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:' | cut -d : -f 2- | sed -e 's/^ //') SUBJECT=$(echo -e "${ORIG_MESSAGE_HEADERS[@]}" | grep -m 1 '^ Subject:' | cut -d : -f 2- | sed -e 's/^ //') } 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 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 GetSubscribersList { #------------------------------------------------------------- # get list subscriber 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): none # depends on function(s): DeclareGpgVars, GetGpgMessage, GetMessageHeaders, # GetGpgDecryptStderr, GetSubscribersList # returns: 0 on success #------------------------------------------------------------- local subscriber local recipients # check if gpg's --hidden-recipient option should be used if [[ "$USE_GPG_HIDDEN_RECIPIENT_OPTION" == "1" ]]; then # get the subscribers' addresses to be used by the GPG_ENCRYPT command bellow and #+possibly by the MAIL_AGENT, in case the message is going to be sent using BCC for subscriber in $(GetSubscribersList); do recipients="$recipients$subscriber " done # this is the body of the message to be sent, so no indentation here MESSAGE_BODY="$(echo "$PASSPHRASE Message from: $FROM Subject: $SUBJECT Date: $DATE $(GetGpgDecryptStderr | grep '^gpg: Signature made') $(GetGpgDecryptStderr | grep '^gpg: Good signature from') $(echo -e "$PASSPHRASE\n${ORIG_GPG_MESSAGE[@]}" | $GPG_DECRYPT 2> /dev/null)" | sed -e 's/=20$//' | $GPG_ENCRYPT --group subscribers="$recipients" --hidden-recipient subscribers 2> /dev/null)" # now send the message, either using BCC to sent it to all subscribers at #+once or sending it separately to each one of them if [[ "$SEND_MESSAGES_USING_BCC" == "1" ]]; then echo -e "From: $LIST_ADDRESS\nBCC: $recipients\nSubject: none\n\n${MESSAGE_BODY[@]}" | sed -e 's/^ //' | $MAIL_AGENT $MAIL_AGENT_ARGS else for subscriber in $recipients; do echo -e "From: $LIST_ADDRESS\nTo: $subscriber\nSubject: none\n\n${MESSAGE_BODY[@]}" | sed -e 's/^ //' | $MAIL_AGENT $MAIL_AGENT_ARGS done fi # else, if gpg's --hidden-recipient option should not be used, #+encrypt and send message separately to each list subscriber else for subscriber in $(GetSubscribersList); do # this is the body of the message to be sent, so no indentation here MESSAGE_BODY="$(echo "$PASSPHRASE Message from: $FROM Subject: $SUBJECT Date: $DATE $(GetGpgDecryptStderr | grep '^gpg: Signature made') $(GetGpgDecryptStderr | grep '^gpg: Good signature from') $(echo -e "$PASSPHRASE\n${ORIG_GPG_MESSAGE[@]}" | $GPG_DECRYPT 2> /dev/null)" | sed -e 's/=20$//' | $GPG_ENCRYPT --recipient $subscriber 2> /dev/null)" # now send the message echo -e "From: $LIST_ADDRESS\nTo: $subscriber\nSubject: none\n\n${MESSAGE_BODY[@]}" | sed -e 's/^ //' | $MAIL_AGENT $MAIL_AGENT_ARGS done fi } function SendWarningMessage { #------------------------------------------------------------- # compose and send a "BAD signature" warning to the list administrator(s) and to sender # # parameter(s): none # depends on function(s): DeclareGpgVars, GetGpgMessage, GetMessageHeaders, # GetGpgDecryptStderr, GetSubscribersList # returns: 0 on success #------------------------------------------------------------- local email_address local recipients # check if gpg's --hidden-recipient option should be used if [[ "$USE_GPG_HIDDEN_RECIPIENT_OPTION" == "1" ]]; then # get the administrator(s) and sender addresses to be used by the GPG_ENCRYPT #+command bellow and possibly by the MAIL_AGENT, in case the message is going #+to be sent using BCC for email_address in $LIST_ADMIN $SENDER_ADDRESS; do recipients="$recipients$email_address " done # this is the body of the message to be sent, so no indentation here MESSAGE_BODY="$(echo "$PASSPHRASE Message from: $FROM Subject: [BAD SIGNATURE] $SUBJECT Date: $DATE $(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 --group addresses="$recipients" --hidden-recipient addresses 2> /dev/null)" # now send the message, either using BCC to sent it to all addresses at #+once or sending it separately to each one of them if [[ "$SEND_MESSAGES_USING_BCC" == "1" ]]; then echo -e "From: $LIST_ADDRESS\nBCC: $recipients\nSubject: none\n\n${MESSAGE_BODY[@]}" | sed -e 's/^ //' | $MAIL_AGENT $MAIL_AGENT_ARGS else for email_address in $LIST_ADMIN $SENDER_ADDRESS; do echo -e "From: $LIST_ADDRESS\nTo: $email_address\nSubject: none\n\n${MESSAGE_BODY[@]}" | sed -e 's/^ //' | $MAIL_AGENT $MAIL_AGENT_ARGS done fi # else, if gpg's --hidden-recipient option should not be used, #+encrypt and send message separately to each address else for email_address in $LIST_ADMIN $SENDER_ADDRESS; do # this is the body of the message to be sent, so no indentation here MESSAGE_BODY="$(echo "$PASSPHRASE Message from: $FROM Subject: [BAD SIGNATURE] $SUBJECT Date: $DATE $(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 --recipient $email_address 2> /dev/null)" # now send the message echo -e "From: $LIST_ADDRESS\nTo: $email_address\nSubject: none\n\n${MESSAGE_BODY[@]}" | sed -e 's/^ //' | $MAIL_AGENT $MAIL_AGENT_ARGS done fi } function SendBounceMessage { #------------------------------------------------------------- # send a bounce message back to sender # # parameter(s): none # depends on function(s): GetMessageHeaders # returns: 0 on success #------------------------------------------------------------- # this is the body of the message to be sent, so no indentation here echo "From: $LIST_ADDRESS To: $SENDER_ADDRESS Subject: none Message from: $FROM Subject: [RETURNED MAIL] $SUBJECT Date: $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_AGENT $MAIL_AGENT_ARGS } function ProcessMessage { #------------------------------------------------------------- # process a received message # # parameter(s): none # depends on function(s): GetMessage, GetMessageHeaders, GetGpgMessage, GetGpgDecryptStderr, # SendListMessage, SendWarningMessage, SendBounceMessage # returns: 0 on success #------------------------------------------------------------- GetMessage GetMessageHeaders GetGpgMessage # if signature in message is valid, encrypt and send it to list subscribers if GetGpgDecryptStderr | grep -q '^\[GNUPG:] GOODSIG'; then SendListMessage # else, if signature is invalid, send it back to the list administrator(s) and to sender elif GetGpgDecryptStderr | grep -q '^\[GNUPG:] BADSIG'; then SendWarningMessage # 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 else SendBounceMessage fi } 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." # commented: # read -rep "path to smtp command (e.g., /usr/sbin/sendmail): " MAIL_AGENT # read -rep "command-line arguments passed to the smtp wrapper (e.g., -oem -oi -t): " MAIL_AGENT_ARGS # read -rep "path to gpg binary (e.g., /usr/bin/gpg): " GPG_BINARY # if [ ! -x "$GPG_BINARY" ]; then # removed: (defaults to $LIST_HOMEDIR) read -rep "list keyring folder: " LIST_HOMEDIR # todo: please no utf-8 (see DETAILS) read -rep "list email (e.g., 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 $LIST_CONFIG_FILE chown root.root $LIST_CONFIG_FILE chmod 600 $LIST_CONFIG_FILE if [ -f "$LIST_CONFIG_FILE" ]; then DeclareGpgVars # removed: MAIL_AGENT=$MAIL_AGENT\nGPG_BINARY=$GPG_BINARY\n echo -e "LIST_HOMEDIR=$LIST_HOMEDIR\nLIST_ADDRESS=$LIST_ADDRESS\nLIST_ADMIN=$LIST_ADMIN\nPASSPHRASE=$PASSPHRASE" > $LIST_CONFIG_FILE echo "now generating your keyring..." $GPG --gen-key <&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 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 # 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 # 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 # 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_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 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() #------------------------------------------------------------- # set environmental variables and options export LANG=en_US umask 0077 # declare global arrays used during execution GLOBAL_ARRAYS="ORIG_MESSAGE ORIG_MESSAGE_HEADERS ORIG_GPG_MESSAGE MESSAGE_BODY" for ARRAY in $GLOBAL_ARRAYS; do declare -a $ARRAY done # 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 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