#!/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: # # USER= user that runs firma (usually the same as your MTA user); # defaults to "nobody"; you can also specify this parameter # in each mailing list config file if you plan to have one # user per mailing list # GROUP= group that runs firma (usually the same as your MTA group); # defaults to "nobody"; you can also specify this parameter # in each mailing list config file if you plan to have one # group per mailing list # LOG_TO_SYSLOG= set to "1" to log errors and warnings to syslog, else firma # will print errors to STDERR # LOGGER_BINARY= if logging to syslog, set the path to logger's binary # SYSLOG_PRIORITY= if logging to syslog, set a priority for the error messages # (defaults to "user.err") # 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) # REMOVE_THESE_HEADERS_ON_ALL_LISTS= headers that should be stripped from list # messages on all lists running under firma # (space separated case-insensitive entries) # (may include regexps (e.g., X-.*) # # 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 # # And it may contain the following optional parameters: # # SUBJECT_PREFIX= prefix to be included in the subject of list messages # REMOVE_THESE_HEADERS= headers that should be stripped from list messages # (space separated case-insensitive entries) # (may include regexps (e.g., X-.*) # REPLIES_SHOULD_GO_TO_LIST= set to '1' to add a Reply-To header containing the # list address # SILENTLY_DISCARD_INVALID_MESSAGES= set to '1' to silently discard invalid # messages (message not signed/encrypted, # sender not subscribed to the list, etc.) # instead of sending bounces back to sender # # 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 4 times. # function Usage { #------------------------------------------------------------- # display help # # parameter(s): none # depends on function(s): none # returns: 0 #------------------------------------------------------------- # this will be printed to STDOUT, so no indentation here echo "\ Usage: $(basename $0) OPTION [LIST-NAME] GnuPG-based encrypted mailing list manager. -a, --admin-task LIST-NAME process administrative tasks on list -c, --create-newlist LIST-NAME create a new mailing list -h, --help display this help and exit -p, --process-message LIST-NAME process a message sent to list -v, --version output version information and exit If option -a is given, read standard input for tasks to be performed. Tasks can be one or more of the following: use EMAIL-ADDRESS use the given address for message delivery instead of the primary address on key Report bugs to , encrypting the message using the pubkey 0xD68AFEDC available at keyserver.noreply.org." } function Version { #------------------------------------------------------------- # display version information # # parameter(s): none # depends on function(s): none # returns: 0 #------------------------------------------------------------- # this will be printed to STDOUT, so no indentation here echo "\ firma $VERSION Copyright (C) 2005 A Firma, Inc. This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. See the GNU General Public License for more details." } function DeclareGpgVars { #------------------------------------------------------------- # declare gpg-related 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-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 --passphrase-fd 0 --no-emit-version --sign --encrypt" } function CheckFirmaConfigFile { #------------------------------------------------------------- # check firma.conf parameters # # parameter(s): none # depends on function(s): none # returns: 0 if all checks are passed, # 1 on any fatal errors #------------------------------------------------------------- local -i return_code=0 if [[ ! -f "$GPG_BINARY" || ! -x "$GPG_BINARY" ]]; then ERROR_MESSAGE="FATAL: GPG binary ("$GPG_BINARY") could not be found. Quitting." return_code=1 elif [[ ! -f "$MAIL_AGENT" || ! -x "$MAIL_AGENT" ]]; then ERROR_MESSAGE="FATAL: Mail transport agent binary ("$MAIL_AGENT") could not be found. Quitting." return_code=1 elif [[ ! -d "$LISTS_DIR" ]]; then ERROR_MESSAGE="FATAL: Lists directory ("$LISTS_DIR") could not be found. Quitting." return_code=1 elif [[ "$USE_GPG_HIDDEN_RECIPIENT_OPTION" == "1" && "$($GPG_BINARY --version | head -n1 | tr -dc '[[:digit:]]')" -lt "140" ]]; then ERROR_MESSAGE="\ WARNING: GPG's \"--hidden-recipient\" option is only available from version 1.4.0 onwards. WARNING: Setting USE_GPG_HIDDEN_RECIPIENT_OPTION to '0'." USE_GPG_HIDDEN_RECIPIENT_OPTION=0 elif [[ "$LOG_TO_SYSLOG" == "1" ]]; then if [[ ! -f "$LOGGER_BINARY" || ! -x "$LOGGER_BINARY" ]]; then ERROR_MESSAGE="\ WARNING: logger binary ("$LOGGER_BINARY") could not be found. WARNING: Setting LOG_TO_SYSLOG to '0'." LOG_TO_SYSLOG=0 fi fi if [ -z "$USER" ]; then USER="nobody" fi if [ -z "$GROUP" ]; then GROUP="nobody" fi return $return_code } function CheckListConfigFile { #------------------------------------------------------------- # check list configuration file parameters # # parameter(s): none # depends on function(s): DeclareGpgVars # returns: 0 if all checks are passed, # 1 on any fatal errors #------------------------------------------------------------- local -i return_code=0 local administrator local valid_admins if [[ ! -d "$LIST_HOMEDIR" || ! -f "$LIST_HOMEDIR/pubring.gpg" || ! -f "$LIST_HOMEDIR/secring.gpg" ]]; then ERROR_MESSAGE="FATAL: $LIST_NAME: GPG home directory ("$LIST_HOMEDIR") or the GPG keyrings could not be found. Quitting." return_code=1 # elif [[ -z "$(grep -o "^PASSPHRASE='[^']*'$" $LIST_CONFIG_FILE)" || \ # -z "$PASSPHRASE" || \ # "$(echo "$PASSPHRASE" | wc -c)" -lt "25" || \ # -z "$(echo "$PASSPHRASE" | tr -dc '[[:lower:]]')" || \ # -z "$(echo "$PASSPHRASE" | tr -dc '[[:upper:]]')" || \ # -z "$(echo "$PASSPHRASE" | tr -dc '[[:digit:]]')" || \ # "$(echo "$PASSPHRASE" | tr -dc '[:punct:]' | wc -c)" -lt "5" || \ # "$(echo "$PASSPHRASE" | fold -w1 | uniq -cd | grep -v '^ \{6\}[234] ')" ]]; then # ERROR_MESSAGE="$LIST_NAME: List passphrase is empty or does not meet the minimum complexity requirements" # return_code=1 elif [[ -z "$($GPG --list-secret-keys --with-colons --fixed-list-mode "<$LIST_ADDRESS>" 2> /dev/null)" ]]; then ERROR_MESSAGE="FATAL: $LIST_NAME: Secret key for list "$LIST_ADDRESS" could not be found. Quitting." return_code=1 else for administrator in $LIST_ADMIN; do { if [[ -z "$($GPG_LIST_KEYS --fixed-list-mode "<$administrator>" 2> /dev/null | grep -v '^tru:')" ]]; then ERROR_MESSAGE="\ WARNING: $LIST_NAME: Public key for list administrator "$administrator" could not be found. WARNING: $LIST_NAME: Removing this address from LIST_ADMIN." else valid_admins="$valid_admins $administrator" fi; } done LIST_ADMIN="$valid_admins" fi return $return_code } function GetMessage { #------------------------------------------------------------- # read message from STDIN # # parameter(s): none # depends on function(s): none # returns: 0 on success, # 1 if there's no input #------------------------------------------------------------- local -i return_code=0 # store message in ORIG_MESSAGE ORIG_MESSAGE="$(sed -ne '1,$p')" # check if message was successfully stored if [[ -z "$ORIG_MESSAGE" ]]; then ERROR_MESSAGE="FATAL: Message couldn't be read from standard input. Quitting." return_code=1 fi return $return_code } function GetGpgMessage { #------------------------------------------------------------- # get the gpg encrypted part of the message # # parameter(s): none # depends on function(s): GetMessage # returns: 0 on success, # 1 if encrypted bloc can't be located within the message #------------------------------------------------------------- local -i return_code=0 # find the first blank line in the message FIRST_BLANK_LINE=$(echo "$ORIG_MESSAGE" | grep -nm 1 '^$' | cut -d : -f 1) # then, find the beginning of the encrypted bloc if [[ -n $FIRST_BLANK_LINE ]]; then ENCRYPTED_BLOC_BEGINS=$(echo "$ORIG_MESSAGE" | grep -nm 1 -- '^-----BEGIN PGP MESSAGE-----' | cut -d : -f 1) # and then find the end of the bloc if [[ -n $ENCRYPTED_BLOC_BEGINS ]]; then ENCRYPTED_BLOC_ENDS=$(echo "$ORIG_MESSAGE" | grep -nm 1 -- '^-----END PGP MESSAGE-----' | cut -d : -f 1) # if there's an encrypted bloc, store it in ORIG_GPG_MESSAGE if [[ -n $ENCRYPTED_BLOC_ENDS ]]; then ORIG_GPG_MESSAGE="$( echo "$ORIG_MESSAGE" | \ sed -ne "$((${ENCRYPTED_BLOC_ENDS} + 1))q;${ENCRYPTED_BLOC_BEGINS},${ENCRYPTED_BLOC_ENDS}p" )" fi fi fi # check if the bloc was successfully stored if [[ -z "$ORIG_GPG_MESSAGE" ]]; then ERROR_MESSAGE="No valid GPG encrypted bloc found within the message" return_code=1 fi return $return_code } function ParseGpgDecryptStderr { #------------------------------------------------------------- # parse $GPG_DECRYPT STDERR for signature checking # # parameter(s): none # depends on function(s): DeclareGpgVars, GetGpgMessage # returns: 0 #------------------------------------------------------------- local gpg_decrypt_stderr # get GPG_DECRYPT STDERR, discarding its STDOUT gpg_decrypt_stderr="$( echo -e "${PASSPHRASE}\n${ORIG_GPG_MESSAGE}" | \ ($GPG_DECRYPT --status-fd 2 1> /dev/null) 2>&1 )" # check if message was encrypted with the list's public key if echo "$gpg_decrypt_stderr" | \ grep -q "^\[GNUPG:] ENC_TO $( $GPG_LIST_KEYS $LIST_ADDRESS 2> /dev/null | \ sed -ne '/:[sca]*[^e][sca]*:$/d' -e '/^sub:[^ired]:/p' | \ cut -d : -f 5 )" then ENCRYPTED_TO_LIST=1 # if it was, check if its signature is valid if echo "$gpg_decrypt_stderr" | \ grep -q '^\[GNUPG:] GOODSIG' then GOOD_SIGNATURE=1 # else, check if the signature is invalid (BAD signature) elif echo "$gpg_decrypt_stderr" | \ grep -q '^\[GNUPG:] BADSIG' then BAD_SIGNATURE=1 # else, check if the signature couldn't be verified elif echo "$gpg_decrypt_stderr" | \ grep -q '^\[GNUPG:] ERRSIG' then SIGNATURE_CHECKING_FAILED=1 # else, check if the message could at least be decrypted elif echo "$gpg_decrypt_stderr" | \ grep -q '^\[GNUPG:] DECRYPTION_OKAY' then MESSAGE_DECRYPTION_OKAY=1 fi fi } 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 valid subscribers on list #------------------------------------------------------------- local -i return_code=0 # get subscribers' email addresses, excluding invalid, revoked, #+expired and disabled keys, as well as any signing only keys SUBSCRIBERS_LIST="$( $GPG_LIST_KEYS 2> /dev/null | \ sed -ne "/$LIST_ADDRESS/Id" -e '/:[scaeSCA]*[^E][scaeSCA]*:$/d' -e '/:[scaeSCAE]*D[scaeSCAE]*:$/d' -e '/^pub:[^ired]:/p' | \ cut -d : -f 10 | \ grep -o '<[^<>]*>$' | \ sed -e 's/[<>]//g' | \ sort -d )" # check if the list has valid subscribers if [[ -z "$SUBSCRIBERS_LIST" ]]; then ERROR_MESSAGE="FATAL: $LIST_NAME: No valid subscribers on list \"$LIST_ADDRESS\". Quitting." return_code=1 fi return $return_code } function GetMessageHeadersAndBody { #------------------------------------------------------------- # store the message headers and body in two separate variables # # parameter(s): none # depends on function(s): GetMessage, GetGpgMessage # returns: 0 #------------------------------------------------------------- # store everything up to the first blank line in ORIG_MESSAGE_HEADERS, #+unfolding any folded headers ORIG_MESSAGE_HEADERS="$( echo "$ORIG_MESSAGE" | \ sed -ne "${FIRST_BLANK_LINE}q;1,$(($FIRST_BLANK_LINE - 1))p" | \ sed -e :a -e '$!N;s/[ \t]*\n[ \t]\+/ /;ta' -e 'P;D' )" # store everything after this line in ORIG_MESSAGE_BODY ORIG_MESSAGE_BODY="$( echo "$ORIG_MESSAGE" | \ sed -ne "$(($FIRST_BLANK_LINE + 1)),\$p" )" } function EditListMessageHeaders { #------------------------------------------------------------- # edit the headers of a list message, removing specific lines, adding #+a prefix to the Subject, etc # # parameter(s): none # depends on function(s): GetMessageHeadersAndBody # returns: 0 #------------------------------------------------------------- local header local sed_args # remove headers as/if defined by firma configuration file if [[ -n "$REMOVE_THESE_HEADERS_ON_ALL_LISTS" ]]; then for header in $REMOVE_THESE_HEADERS_ON_ALL_LISTS; do sed_args="$sed_args -e /^${header}/Id" done MESSAGE_HEADERS="$( echo "$ORIG_MESSAGE_HEADERS" | \ sed $sed_args )" fi # remove additional headers as/if defined by the list configuration file if [[ -n "$REMOVE_THESE_HEADERS" ]]; then # remove local variables contents, in case they have been used above header='' sed_args='' for header in $REMOVE_THESE_HEADERS; do sed_args="$sed_args -e /^${header}/Id" done MESSAGE_HEADERS="$( echo "$MESSAGE_HEADERS" | \ sed $sed_args )" fi # insert/replace the Reply-To header if [[ -n "$REPLIES_SHOULD_GO_TO_LIST" ]]; then if ! echo "$MESSAGE_HEADERS" | \ grep -iq '^Reply-To:'; then # these are the headers of the message to be sent, so no indentation here MESSAGE_HEADERS="\ $MESSAGE_HEADERS Reply-To: $LIST_ADDRESS" else MESSAGE_HEADERS="$( echo "$MESSAGE_HEADERS" | \ sed -e "s/^Reply-To:.*$/Reply-To: $LIST_ADDRESS/I" )" fi fi # insert the Subject prefix, if any if [[ -n "$SUBJECT_PREFIX" ]]; then # first, check if there's a Subject line if echo "$MESSAGE_HEADERS" | grep -iq '^Subject:'; then # and then check if the Subject already contains the list prefix if ! echo "$MESSAGE_HEADERS" | \ grep -im 1 '^Subject:' | \ grep -qF "$SUBJECT_PREFIX"; then # if it doesn't, insert it MESSAGE_HEADERS="$( echo "$MESSAGE_HEADERS" | \ sed -e "s/^Subject:[ \t]*/Subject: $SUBJECT_PREFIX/I" )" fi # else, if there's no Subject line, add one containing only the list prefix else # these are the headers of the message to be sent, so no indentation here MESSAGE_HEADERS="\ $MESSAGE_HEADERS Subject: $SUBJECT_PREFIX" fi fi } function DecryptGpgMessage { #------------------------------------------------------------- # decrypt the gpg encrypted part of the message # # parameter(s): none # depends on function(s): DeclareGpgVars, GetGpgMessage # returns: 0 #------------------------------------------------------------- DECRYPTED_MESSAGE="$( echo -e "${PASSPHRASE}\n${ORIG_GPG_MESSAGE}" | \ $GPG_DECRYPT 2> /dev/null )" } function ReplaceGpgMessage { #------------------------------------------------------------- # replace the original encrypted bloc by one generated for the list subscribers # # parameter(s): none # depends on function(s): GetGpgMessage, GetMessageHeadersAndBody # returns: 0 #------------------------------------------------------------- MESSAGE_BODY="$( echo "$ORIG_MESSAGE_BODY" | \ sed -e "$(($ENCRYPTED_BLOC_BEGINS - $FIRST_BLANK_LINE)),$(($ENCRYPTED_BLOC_ENDS - $FIRST_BLANK_LINE))c $( echo "$GPG_MESSAGE" | \ sed -e '$! s/$/\\/' )" )" } function GetSenderAddress { #------------------------------------------------------------- # get the sender address, needed for warning and bounce messages processing # # parameter(s): none # depends on function(s): GetMessage # returns: 0 #------------------------------------------------------------- local from from=$(echo "$ORIG_MESSAGE" | grep -im 1 '^From:') SENDER_ADDRESS=$( if [[ -z "$(echo $from | grep '>$')" ]]; then echo $from else echo $from | grep -o '<[^<>]*>$' | sed -e 's/[<>]//g' fi ) } function AssembleMessage { #------------------------------------------------------------- # just put the message headers and body together # # parameter(s): none # depends on function(s): EditListMessageHeaders, ReplaceGpgMessage, # ComposeAndSendWarningMessage, # ComposeAndSendBounceMessage # returns: 0 #------------------------------------------------------------- # this is the actual message which will be sent, so no indentation here MESSAGE="\ $MESSAGE_HEADERS $MESSAGE_BODY" } function ReEncryptAndSendListMessage { #------------------------------------------------------------- # send message to list subscribers # # parameter(s): none # depends on function(s): DeclareGpgVars, DecryptGpgMessage, # GetSubscribersList, AssembleMessage # returns: 0 #------------------------------------------------------------- local recipients local subscriber recipients="$(echo $SUBSCRIBERS_LIST)" # check if message should be encrypted and sent to all subscribers at once if [[ "$USE_GPG_HIDDEN_RECIPIENT_OPTION" == 1 ]]; then GPG_MESSAGE="$( echo -e "${PASSPHRASE}\n${DECRYPTED_MESSAGE}" | \ $GPG_ENCRYPT --group subscribers="$recipients" --hidden-recipient subscribers 2> /dev/null )" ReplaceGpgMessage AssembleMessage # send message echo "$MESSAGE" | $MAIL_AGENT $MAIL_AGENT_ARGS $recipients # else, message should be encrypted and sent to one subscriber at a time else for subscriber in $recipients; do GPG_MESSAGE="$( echo -e "${PASSPHRASE}\n${DECRYPTED_MESSAGE}" | \ $GPG_ENCRYPT --recipient $subscriber )" ReplaceGpgMessage AssembleMessage # send message echo "$MESSAGE" | $MAIL_AGENT $MAIL_AGENT_ARGS $subscriber done fi } function ComposeAndSendWarningMessage { #------------------------------------------------------------- # send a "BAD signature" warning to the list administrator(s) and to sender # # parameter(s): none # depends on function(s): GetMessage, GetSenderAddress, AssembleMessage # returns: 0 #------------------------------------------------------------- local recipients recipients="$LIST_ADMIN $SENDER_ADDRESS" # these are the headers of the message to be sent, so no indentation here MESSAGE_HEADERS="\ From: $LIST_ADDRESS Subject: BAD signature from $SENDER_ADDRESS To: $SENDER_ADDRESS Cc: $(echo $LIST_ADMIN | sed -e 's/ /, /g')" # this is the body of the message to be sent, so no indentation here MESSAGE_BODY="\ -------- Original Message -------- $ORIG_MESSAGE" AssembleMessage # send message echo "$MESSAGE" | $MAIL_AGENT $MAIL_AGENT_ARGS $recipients } function ComposeAndSendBounceMessage { #------------------------------------------------------------- # send a bounce message back to sender # # parameter(s): none # depends on function(s): GetMessage, GetSenderAddress, AssembleMessage # returns: 0 #------------------------------------------------------------- local subject subject="$(echo "$ORIG_MESSAGE" | grep -im 1 '^Subject:' | cut -d : -f 2- )" # these are the headers of the message to be sent, so no indentation here MESSAGE_HEADERS="\ From: $LIST_ADDRESS Subject: [RETURNED MAIL]$subject To: $SENDER_ADDRESS" # this is the body of the message to be sent, so no indentation here MESSAGE_BODY="\ $MESSAGE_BODY -- firma" AssembleMessage # send message echo "$MESSAGE" | $MAIL_AGENT $MAIL_AGENT_ARGS $SENDER_ADDRESS } function ProcessMessage { #------------------------------------------------------------- # process a received message # # parameter(s): none # depends on function(s): GetMessage, GetGpgMessage, GetSubscribersList, # GetSenderAddress # returns: 0 on success, # 1 if any of the above functions return an error #------------------------------------------------------------- local -i return_code=0 # try to read message from STDIN if GetMessage; then # check if the message was encrypted if GetGpgMessage; then # if it was, parse gpg decrypt STDERR to decide what to do next ParseGpgDecryptStderr # if the message was encrypted with the list's public key and if the #+message signature is valid, send message to list subscribers if [[ $ENCRYPTED_TO_LIST == 1 && $GOOD_SIGNATURE == 1 ]]; then # check if the list has valid subscribers if GetSubscribersList; then GetMessageHeadersAndBody EditListMessageHeaders DecryptGpgMessage ReEncryptAndSendListMessage else return_code=1 fi # else, if the message was correctly encrypted but its signature is invalid, #+send a warning about this to the list administrator(s) and to sender elif [[ $ENCRYPTED_TO_LIST == 1 && $BAD_SIGNATURE == 1 ]]; then GetSenderAddress if [[ -n $(echo $LIST_ADMINS) || -n "$SENDER_ADDRESS" ]]; then ComposeAndSendWarningMessage fi # else, a bounce should be sent else # if bounce processing is enabled, continue if [[ "$SILENTLY_DISCARD_INVALID_MESSAGES" != 1 ]]; then GetSenderAddress if [[ -n "$SENDER_ADDRESS" ]]; then # if the message was encrypted with the list's public key if [[ $ENCRYPTED_TO_LIST == 1 ]]; then # then, if signature can't be checked, then probably the sender is not subscribed to the list # send a bounce, if possible if [[ $SIGNATURE_CHECKING_FAILED == 1 ]]; then # this is the body of the message to be sent, so no indentation here MESSAGE_BODY="\ It was not possible to process this message. Your email address is not subscribed to this list. Contact the list administrator if you have any questions." ComposeAndSendBounceMessage # or, if message can be decrypted but its signature can't be checked, then message wasn't signed # send a bounce, if possible elif [[ $MESSAGE_DECRYPTION_OKAY == 1 ]]; then # this is the body of the message to be sent, so no indentation here MESSAGE_BODY="\ It was not possible to process this message. Message was not signed. Contact the list administrator if you have any questions." ComposeAndSendBounceMessage fi # else, message wasn't encrypted with the list's public key # send a bounce, if possible else # this is the body of the message to be sent, so no indentation here MESSAGE_BODY="\ It was not possible to process this message. Message was not encrypted with the list's public key. Contact the list administrator if you have any questions." ComposeAndSendBounceMessage fi fi fi fi # else, message wasn't encrypted at all # send a bounce, if possible else # if bounce processing is enabled, continue if [[ "$SILENTLY_DISCARD_INVALID_MESSAGES" != 1 ]]; then GetSenderAddress if [[ -n "$SENDER_ADDRESS" ]]; then # this is the body of the message to be sent, so no indentation here MESSAGE_BODY="\ It was not possible to process this message. Message was not encrypted. Contact the list administrator if you have have any questions." ComposeAndSendBounceMessage fi fi fi # else, message could not be read from STDIN else return_code=1 fi return $return_code } 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 #------------------------------------------------------------- local -i return_code=0 if [ ! -d "$LIST_PATH" ]; then echo "Creating folder $LIST_PATH..." if mkdir "$LIST_PATH"; then # || (echo "$(basename $0): error creating $LIST_PATH: installation aborted"; exit 1) echo "Creating list config file and will ask some questions." read -rep " List keyring location: ("$LIST_PATH") " LIST_HOMEDIR LIST_HOMEDIR=${LIST_HOMEDIR:-"$LIST_PATH"} # Dont use UTF-8 (look at DETAILS) read -rep " List email address: " LIST_ADDRESS read -rep " List administrator(s) email address(es) (space delimited): " LIST_ADMIN read -rep " List description (optional): " DESCRIPTION read -resp " Passphrase to protect the list's secret key: " PASSPHRASE # TODO: key specs (size, expiry date...) echo "Creating your config..." touch $LIST_CONFIG_FILE chmod 600 $LIST_CONFIG_FILE chown $USER.$GROUP $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_code=1 ;; unsub) echo >&2 "$1: missing arguments (try \"help\")" return_code=1 ;; *) echo >&2 "Command not found -- $1 (try \"help\")" return_code=1 ;; esac ;; 2) case $1 in use) # check if argument is an email address if CheckValidEmail $2; then ChooseUid $2 else echo >&2 "$1: invalid argument -- $2 (try \"help\")" return_code=1 fi ;; unsub) # check if argument is an email address if CheckValidEmail $2; then UnsubscribeUser $2 else echo >&2 "$1: invalid argument -- $2 (try \"help\")" return_code=1 fi ;; help|quit) echo >&2 "$1: too many arguments -- $@ (try \"help\")" return_code=1 ;; *) echo >&2 "Command not found -- $1 (try \"help\")" return_code=1 ;; esac ;; *) case $1 in help|quit|use) echo >&2 "$1: too many arguments -- $@ (try \"help\")" return_code=1 ;; *) echo >&2 "Command not found -- $1 (try \"help\")" return_code=1 ;; esac ;; esac return $return_code } function ChooseUid { #------------------------------------------------------------- # choose which UID of a public key should be used for message delivery, #+deleting all other UIDs on the 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 -i return_code=0 local keyid="$($GPG_LIST_KEYS --with-fingerprint $1 2> /dev/null | grep ^fpr | cut -d : -f 10)" 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 -v '^tru:')" ]]; then echo >&2 "use: \"$1\" is not associated with any public key on this keyring." return_code=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)" == 1 )); then echo >&2 "use: \"$1\" is part of the only UID on public key ${keyid:32}." return_code=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)" > 1 )); then echo >&2 "use: \"$1\" 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_code=1 fi # if all checks are OK, run the expect script bellow if (( $return_code == 0 )); then 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: \"$1\" chosen for message delivery. [ expr $uid_count - 1 ] UID(s) deleted from public key ${keyid:32}.\n" exit EOF fi return $return_code } function CheckPermission { #------------------------------------------------------------- # check if file has correct permissions (600) and also # + if the file is owned by $USER # +got the idea for this function from backupninja # # parameter(s): file name # depends on function(s): none # returns: 0 if file has correct permissions # 1 if not, and also print a warning message #------------------------------------------------------------- local file="$1" local perms="`ls -ld $file`" perms=${perms:4:6} if [ "$perms" != "------" ]; then LogMessage "WARNING: Configuration files must not be group or world writable/readable! Dying on file $file" return 1 fi if [ `ls -ld $file | awk '{print $3}'` != "$USER" ]; then echo "WARNING: Configuration files must be owned by $USER! Dying on file $file" fi return 0 } function CheckListPermissions { #------------------------------------------------------------- # check if list files has correct permissions (600) and also # + if the files are owned by $USER # # parameter(s): list config file # depends on function(s): CheckPermission # returns: 0 if file has correct permissions # 1 if not, and also print a warning message #------------------------------------------------------------- local file local folder local config # check and fix permissions on all files from $LIST_PATH to $USER.$GROUP if [ ! -z "$1" ]; then folder="`dirname $1`" config="`basename $1`" for file in $config pubring.gpg pubring.gpg~ random_seed secring.gpg trustdb.gpg; do if ! CheckPermission $folder/$file; then LogMessage "Fixing permission and ownership for $folder/$file" chmod 600 $folder/$file chown $USER.$GROUP $folder/$file fi done fi } function CheckValidEmail { #------------------------------------------------------------- # check if argument is a valid email address # # parameter(s): string # depends on function(s): none # returns: 0 if string represents a valid email address # 1 if not #------------------------------------------------------------- if ! echo $2 | grep -q '[^@]\+@[^@]\+'; then return 1 else return 0 fi } function UnsubscribeUser { # TODO: usubscribe if $1 is subscriber # always fix list folder permissions true } function LogMessage { #------------------------------------------------------------- # write a log message to stdout or to syslog # # parameter(s): string # depends on function(s): none # returns: 0 #------------------------------------------------------------- local error_message error_message="$*" if [[ "$LOG_TO_SYSLOG" == 1 ]]; then echo "$error_message" | $LOGGER_BINARY -p "$SYSLOG_PRIORITY" -t "$BASENAME" else echo >&2 "$BASENAME: $error_message" fi return 0 } #------------------------------------------------------------- # main() #------------------------------------------------------------- # path to firma.conf and firma version FIRMA_CONFIG_FILE="/usr/local/etc/firma.conf" VERSION="0.3" # set environmental variables and options export LANG=en_US umask 0077 # declare global variables and functions used during execution GLOBAL_VARS=" GPG_BINARY MAIL_AGENT MAIL_AGENT_ARGS LISTS_DIR LOG_TO_SYSLOG LOGGER_BINARY SYSLOG_PRIORITY USE_GPG_HIDDEN_RECIPIENT_OPTION REMOVE_THESE_HEADERS_ON_ALL_LISTS SILENTLY_DISCARD_INVALID_MESSAGES LIST_ADDRESS LIST_ADMIN LIST_HOMEDIR PASSPHRASE SUBJECT_PREFIX REMOVE_THESE_HEADERS REPLIES_SHOULD_GO_TO_LIST FIRMA_CONFIG_FILE VERSION ERROR_MESSAGE EXIT_CODE DESCRIPTION LIST_NAME LIST_PATH LIST_CONFIG_FILE GPG_FLAGS GPG GPG_LIST_KEYS GPG_DECRYPT GPG_ENCRYPT STDIN ORIG_MESSAGE FIRST_BLANK_LINE ENCRYPTED_BLOC_BEGINS ENCRYPTED_BLOC_ENDS ORIG_GPG_MESSAGE ENCRYPTED_TO_LIST GOOD_SIGNATURE BAD_SIGNATURE SIGNATURE_CHECKING_FAILED MESSAGE_DECRYPTION_OKAY SUBSCRIBERS_LIST ORIG_MESSAGE_HEADERS ORIG_MESSAGE_BODY GPG_MESSAGE DECRYPTED_MESSAGE MESSAGE_HEADERS MESSAGE_BODY MESSAGE FUNCTION FUNCTIONS GLOBAL_VARS VAR USER GROUP BASENAME" FUNCTIONS=" Usage Version DeclareGpgVars CheckFirmaConfigFile CheckListConfigFile GetMessage GetGpgMessage ParseGpgDecryptStderr GetSubscribersList GetMessageHeadersAndBody EditListMessageHeaders DecryptGpgMessage ReplaceGpgMessage GetSenderAddress AssembleMessage ReEncryptAndSendListMessage ComposeAndSendWarningMessage ComposeAndSendBounceMessage ProcessMessage NewList ListAdministration ChooseUid CheckPermission CheckListPermissions UnsubscribeUser LogMessage" for VAR in $GLOBAL_VARS; do declare $VAR done # set initial exit code EXIT_CODE=0 # set program name BASENAME="`basename $0`" # 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_CODE=1 ;; 1) # start case #1 case $1 in -h|--help) Usage EXIT_CODE=0 ;; -v|--version) Version EXIT_CODE=0 ;; # valid option called without its required argument -a|--admin-task|-c|--create-newlist|-p|--process-message) echo >&2 "$(basename $0): missing arguments" Usage EXIT_CODE=1 ;; *) echo >&2 "$(basename $0): invalid option -- $1" Usage EXIT_CODE=1 ;; # end case #1 esac ;; 2) # if firma.conf exists but has wrong permissions or ownership if [ -f "$FIRMA_CONFIG_FILE" ] && ! CheckPermission $FIRMA_CONFIG_FILE; then EXIT_CODE="1" # if firma.conf exists elif [ -f "$FIRMA_CONFIG_FILE" ]; then # evaluate its parameters shopt -u sourcepath && source "$FIRMA_CONFIG_FILE" # set SYSLOG_PRIORITY to the default value, if needed if [[ "$LOG_TO_SYSLOG" == 1 ]]; then SYSLOG_PRIORITY=${SYSLOG_PRIORITY:-"user.err"} fi # and finally check firma.conf if CheckFirmaConfigFile; then LIST_NAME="$2" LIST_PATH="$LISTS_DIR/$LIST_NAME" LIST_CONFIG_FILE="$LIST_PATH/$LIST_NAME.conf" # start case #2 case $1 in -c|--create-newlist) NewList ;; # options that depend on the list configuration file -a|--admin-task|-p|--process-message) # if config file exists but has wrong permissions or ownership if [[ -f "$LIST_CONFIG_FILE" ]] && ! CheckPermission $LIST_CONFIG_FILE; then EXIT_CODE="1" elif [[ -f "$LIST_CONFIG_FILE" ]]; then # if the configuration file exists, disable bash's #+sourcepath and evaluate list parameters shopt -u sourcepath && source "$LIST_CONFIG_FILE" CheckListPermissions $LIST_CONFIG_FILE # get gpg parameters DeclareGpgVars # check the list configuration file if CheckListConfigFile; then # start case #3 case $1 in -a|--admin-task) # while a quit command isn't entered (returns 2), read STDIN while (( $EXIT_CODE != 2 )) && read -rep "Command> " STDIN; do # if line is not empty or commented, process command if [[ -n "$STDIN" && "$STDIN" != "#"* ]]; then ListAdministration $STDIN EXIT_CODE=$? fi done # since quit was entered, exit without error EXIT_CODE=0 ;; -p|--process-message) ProcessMessage EXIT_CODE=$? ;; # end case #3 esac # else, list configuration file checking returned an error else EXIT_CODE=$? fi # else, list configuration file could not be found else ERROR_MESSAGE="Cannot source \`$LIST_CONFIG_FILE': No such file or directory" EXIT_CODE=1 fi ;; # valid option called with too many arguments -h|--help|-v|--version) echo >&2 "$(basename $0): too many arguments -- $@" Usage EXIT_CODE=1 ;; *) echo >&2 "$(basename $0): invalid option -- $1" Usage EXIT_CODE=1 ;; # end case #2 esac # else, firma.conf checking returned an error else EXIT_CODE=$? fi # else, firma.conf could not be found else ERROR_MESSAGE="Cannot source \`$FIRMA_CONFIG_FILE': No such file or directory" EXIT_CODE=1 fi ;; *) # 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|-v|--version) echo >&2 "$(basename $0): too many arguments -- $@" Usage EXIT_CODE=1 ;; *) echo >&2 "$(basename $0): invalid option -- $1" Usage EXIT_CODE=1 ;; # end case #4 esac ;; # end main case esac # print/log error message, if any if [[ -n "$ERROR_MESSAGE" ]]; then LogMessage $ERROR_MESSAGE fi # erase all functions and global variables for FUNCTION in $FUNCTIONS; do unset -f $FUNCTION done for VAR in $GLOBAL_VARS; do unset $VAR done # exit exit $EXIT_CODE