#!/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 # 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 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 -e, --email-admin-task LIST-NAME process administrative tasks via email -h, --help display 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: $(AdminHelp) For help with admin and config paramaters, type $BASENAME --help task-name Report bugs to , encrypting your message using the public key 3DF104314023E18358EFDD270D51B4E79E693CF7, available at keys.indymedia.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-2007 A Firma 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 --no-tty --batch --no-use-agent --no-permission-warning" GPG_FLAGS_NO_BATCH="--no-options --no-default-keyring --homedir $LIST_HOMEDIR --quiet --no-batch --no-use-agent --no-permission-warning" GPG="$GPG_BINARY $GPG_FLAGS" GPG_NOBATCH="$GPG_BINARY $GPG_FLAGS_NO_BATCH" 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 CheckPassphrase { #------------------------------------------------------------- # check if a passphrase is valid: is 25 characters long or more; #+includes lower and upper case letters, numbers and at least 1 #+punctuation character; and no character is sequentially #+repeated more than 4 times. # # parameter(s): none # depends on function(s): none # returns: 0 if passphrase is valid, # 1 if invalid #------------------------------------------------------------- local -i return_code=0 if [[ -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 "1" || \ -n "$(echo "$PASSPHRASE" | fold -w1 | uniq -cd | grep -v '^ \{6\}[234] ')" ]]; then return_code=1 fi return $return_code } 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 local gpg_version # check LOG_TO_SYSLOG value first, since it will define if firma #+should print or log error messages if [[ -n "$LOG_TO_SYSLOG" && \ "$LOG_TO_SYSLOG" != "0" && \ "$LOG_TO_SYSLOG" != "1" ]]; then LOG_TO_SYSLOG="0" LogMessage "\ WARNING: LOG_TO_SYSLOG should be set either to '0' or '1'. WARNING: Setting LOG_TO_SYSLOG to '0' for this run." elif [[ -z "$LOG_TO_SYSLOG" ]]; then LOG_TO_SYSLOG="0" elif [[ "$LOG_TO_SYSLOG" == "1" ]]; then if [[ ! -f "$LOGGER_BINARY" || ! -x "$LOGGER_BINARY" ]]; then LOG_TO_SYSLOG="0" LogMessage "\ WARNING: Logger binary ($LOGGER_BINARY) could not be found. WARNING: Setting LOG_TO_SYSLOG to '0' for this run." else # SYSLOG_PRIORITY defaults to "user.err" SYSLOG_PRIORITY=${SYSLOG_PRIORITY:-"user.err"} fi fi # check GPG_BINARY value if [[ ! -f "$GPG_BINARY" || ! -x "$GPG_BINARY" ]]; then LogMessage "FATAL: GPG binary ($GPG_BINARY) could not be found. Quitting." return_code=1 # check MAIL_AGENT value elif [[ ! -f "$MAIL_AGENT" || ! -x "$MAIL_AGENT" ]]; then LogMessage "FATAL: Mail transport agent binary ($MAIL_AGENT) could not be found. Quitting." return_code=1 # check LISTS_DIR value elif [[ ! -d "$LISTS_DIR" ]]; then LogMessage "FATAL: Lists directory ($LISTS_DIR) could not be found. Quitting." return_code=1 # optional parameters else # check USE_GPG_HIDDEN_RECIPIENT_OPTION value if [[ -n "$USE_GPG_HIDDEN_RECIPIENT_OPTION" && \ "$USE_GPG_HIDDEN_RECIPIENT_OPTION" != "0" && \ "$USE_GPG_HIDDEN_RECIPIENT_OPTION" != "1" ]]; then LogMessage "\ WARNING: USE_GPG_HIDDEN_RECIPIENT_OPTION should be set either to '0' or '1'. WARNING: Setting USE_GPG_HIDDEN_RECIPIENT_OPTION to '0' for this run." USE_GPG_HIDDEN_RECIPIENT_OPTION="0" elif [[ -z "$USE_GPG_HIDDEN_RECIPIENT_OPTION" ]]; then USE_GPG_HIDDEN_RECIPIENT_OPTION="0" elif [[ "$USE_GPG_HIDDEN_RECIPIENT_OPTION" == "1" ]]; then gpg_version="$($GPG_BINARY --version | head -n 1 | tr -dc '[:digit:]')" if [[ "$gpg_version" -lt "140" ]]; then LogMessage "\ WARNING: GPG's \"--hidden-recipient\" option is only available from version 1.4.0 onwards. WARNING: Setting USE_GPG_HIDDEN_RECIPIENT_OPTION to '0' for this run." USE_GPG_HIDDEN_RECIPIENT_OPTION="0" fi fi # check FIRMA_USER value if [[ -z "$(echo "$FIRMA_USER" | tr -d '[:space:]')" ]]; then FIRMA_USER="nobody" fi # check FIRMA_GROUP value if [[ -z "$(echo "$FIRMA_GROUP" | tr -d '[:space:]')" ]]; then FIRMA_GROUP="nobody" fi # check KEYSERVER value if [[ -z "$(echo "$KEYSERVER" | tr -d '[:space:]')" ]]; then KEYSERVER="keys.indymedia.org" fi 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 local replay_default_file="/var/log/firma/replay.db" # check LIST_HOMEDIR value if [[ ! -d "$LIST_HOMEDIR" || \ ! -f "$LIST_HOMEDIR/pubring.gpg" || \ ! -f "$LIST_HOMEDIR/secring.gpg" ]]; then LogMessage "FATAL: $LIST_NAME: GPG home directory ($LIST_HOMEDIR) or the GPG keyrings could not be found. Quitting." return_code=1 # check PASSPHRASE value elif [[ -z "$(grep -o "^PASSPHRASE='[^']*'$" $LIST_CONFIG_FILE)" ]] || \ ! CheckPassphrase; then LogMessage "FATAL: $LIST_NAME: List passphrase is empty or does not meet the minimum complexity requirements. Quitting." return_code=1 # check LIST_ADDRESS value, confirming if the list private key is present elif [[ -z "$($GPG --list-secret-keys --with-colons --fixed-list-mode "<$LIST_ADDRESS>" 2> /dev/null)" ]]; then LogMessage "FATAL: $LIST_NAME: List's secret key could not be found. Quitting." return_code=1 # optional parameters else # check if the list has an administrator (or more than one) if [[ -z "$(echo "$LIST_ADMIN" | tr -d '[:space:]')" ]]; then LogMessage "WARNING: $LIST_NAME: List has no administrator." LIST_ADMIN="" else # check if the public key(s) of the list administrator(s) is(are) present valid_admins="" for administrator in $LIST_ADMIN; do if [[ -z "$($GPG_LIST_KEYS --fixed-list-mode "<$administrator>" 2> /dev/null | \ grep -v '^tru:')" ]]; then LogMessage "\ WARNING: $LIST_NAME: Public key for list administrator \"$administrator\" could not be found. WARNING: $LIST_NAME: Removing this address from LIST_ADMIN for this run." else valid_admins="$valid_admins $administrator" fi done LIST_ADMIN="$valid_admins" if [[ -z "$(echo "$LIST_ADMIN" | tr -d '[:space:]')" ]]; then LogMessage "WARNING: $LIST_NAME: List has no valid administrator." LIST_ADMIN="" fi fi # check if LIST_REQUEST_ADDRESS has already been set if [[ -z "$(echo "$LIST_REQUEST_ADDRESS" | tr -d '[:space:]')" ]]; then LIST_REQUEST_ADDRESS="$(echo $LIST_ADDRESS | cut -d @ -f 1)-request@$(echo $LIST_ADDRESS | cut -d @ -f 2)" fi # check REQUIRE_SIGNATURE value if [[ -n "$REQUIRE_SIGNATURE" && \ "$REQUIRE_SIGNATURE" != "0" && \ "$REQUIRE_SIGNATURE" != "1" ]]; then LogMessage "\ WARNING: $LIST_NAME: REQUIRE_SIGNATURE should be set either to '0' or '1'. WARNING: $LIST_NAME: Setting REQUIRE_SIGNATURE to '1' for this run." REQUIRE_SIGNATURE="1" elif [[ -z "$REQUIRE_SIGNATURE" ]]; then REQUIRE_SIGNATURE="1" fi # check REPLIES_SHOULD_GO_TO_LIST value if [[ -n "$REPLIES_SHOULD_GO_TO_LIST" && \ "$REPLIES_SHOULD_GO_TO_LIST" != "0" && \ "$REPLIES_SHOULD_GO_TO_LIST" != "1" ]]; then LogMessage "\ WARNING: $LIST_NAME: REPLIES_SHOULD_GO_TO_LIST should be set either to '0' or '1'. WARNING: $LIST_NAME: Setting REPLIES_SHOULD_GO_TO_LIST to '0' for this run." REPLIES_SHOULD_GO_TO_LIST="0" elif [[ -z "$REPLIES_SHOULD_GO_TO_LIST" ]]; then REPLIES_SHOULD_GO_TO_LIST="0" fi # check HIDE_SENDER value if [[ -n "$HIDE_SENDER" && \ "$HIDE_SENDER" != "0" && \ "$HIDE_SENDER" != "1" ]]; then LogMessage "\ WARNING: $LIST_NAME: HIDE_SENDER should be set either to '0' or '1'. WARNING: $LIST_NAME: Setting HIDE_SENDER to '0' for this run." HIDE_SENDER="0" elif [[ -z "$HIDE_SENDER" ]]; then HIDE_SENDER="0" fi # check REPLAY_PROTECTION value if [[ -n "$REPLAY_PROTECTION" && \ "$REPLAY_PROTECTION" != "0" && \ "$REPLAY_PROTECTION" != "1" ]]; then LogMessage "\ WARNING: $LIST_NAME: REPLAY_PROTECTION should be set either to '0' or '1'. WARNING: $LIST_NAME: Setting REPLAY_PROTECTION to '0' for this run." REPLAY_PROTECTION="0" elif [[ -z "$REPLAY_PROTECTION" ]]; then REPLAY_PROTECTION="0" elif [[ "$REPLAY_PROTECTION" == "1" ]]; then # check REPLAY_COUNT value if [[ -n "$REPLAY_COUNT" && \ -n "$(echo "$REPLAY_COUNT" | tr -d '[:digit:]')" ]]; then LogMessage "\ WARNING: $LIST_NAME: REPLAY_COUNT should be a number. WARNING: $LIST_NAME: Setting REPLAY_COUNT to '150' for this run." REPLAY_COUNT="150" elif [[ -z "$REPLAY_COUNT" ]]; then REPLAY_COUNT="150" else # REPLAY_COUNT is either set to 0 (defaults to 150) or #+contains a valid value REPLAY_COUNT="$(( 10#$(echo "$REPLAY_COUNT" | tr -dc '[:digit:]') ))" if [[ "$REPLAY_COUNT" == "0" ]]; then LogMessage "\ WARNING: $LIST_NAME: REPLAY_COUNT has to be greater than '0'. WARNING: $LIST_NAME: Setting REPLAY_COUNT to '150' for this run." REPLAY_COUNT="150" fi fi # check REPLAY_FILE value if [[ -z "$(echo "$REPLAY_FILE" | tr -d '[:space:]')" ]]; then REPLAY_FILE="$replay_default_file" fi touch "$REPLAY_FILE" 2> /dev/null chown "$FIRMA_USER":"$FIRMA_GROUP" "$REPLAY_FILE" 2> /dev/null chmod 600 "$REPLAY_FILE" 2> /dev/null if [[ ! -r "$REPLAY_FILE" || ! -w "$REPLAY_FILE" ]]; then LogMessage "\ WARNING: $LIST_NAME: REPLAY_FILE ($REPLAY_FILE) can't be read or written to. WARNING: $LIST_NAME: Setting REPLAY_PROTECTION to '0' for this run." REPLAY_PROTECTION="0" fi fi # check DELIVERY_RANDOMIZATION value if [[ -n "$DELIVERY_RANDOMIZATION" && \ -n "$(echo "$DELIVERY_RANDOMIZATION" | tr -d '[:digit:]')" ]]; then LogMessage "\ WARNING: $LIST_NAME: DELIVERY_RANDOMIZATION should be a number. WARNING: $LIST_NAME: Setting DELIVERY_RANDOMIZATION to '0' for this run." DELIVERY_RANDOMIZATION="0" else # DELIVERY_RANDOMIZATION is either empty (defaults to 0) or #+contains a valid value DELIVERY_RANDOMIZATION="$(( 10#$(echo $DELIVERY_RANDOMIZATION | tr -dc '[:digit:]') ))" fi # check SILENTLY_DISCARD_INVALID_MESSAGES value if [[ -n "$SILENTLY_DISCARD_INVALID_MESSAGES" && \ "$SILENTLY_DISCARD_INVALID_MESSAGES" != "0" && \ "$SILENTLY_DISCARD_INVALID_MESSAGES" != "1" ]]; then LogMessage "\ WARNING: $LIST_NAME: SILENTLY_DISCARD_INVALID_MESSAGES should be set either to '0' or '1'. WARNING: $LIST_NAME: Setting SILENTLY_DISCARD_INVALID_MESSAGES to '0' for this run." SILENTLY_DISCARD_INVALID_MESSAGES="0" elif [[ -z "$SILENTLY_DISCARD_INVALID_MESSAGES" ]]; then SILENTLY_DISCARD_INVALID_MESSAGES="0" fi 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 LogMessage "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 LogMessage "WARNING: 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, GetSenderAddress # 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" if [[ -z "$SENDER_ADDRESS" ]]; then GetSenderAddress fi if echo "$gpg_decrypt_stderr" | \ grep '^\[GNUPG:] GOODSIG' | \ grep -q "$SENDER_ADDRESS" then SIGNATURE_MADE_BY_SENDER="1" else SIGNATURE_MADE_BY_SENDER="0" fi # 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 LogMessage "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 MESSAGE_HEADERS="$ORIG_MESSAGE_HEADERS" # 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 "$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 [[ "$REPLIES_SHOULD_GO_TO_LIST" == "1" ]]; 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 # hide the sender if [[ "$HIDE_SENDER" == "1" ]]; then MESSAGE_HEADERS="$( echo "$MESSAGE_HEADERS" | \ sed -e "s/^From:.*$/From: $LIST_ADDRESS/I" )" 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 DeliveryRandomization # 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 DeliveryRandomization # 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 # look for replay attacks if ReplayProtectionCheck; 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 AllowMessageProcessing; then # check if the list has valid subscribers GetSenderAddress GetMessageHeadersAndBody EditListMessageHeaders DecryptGpgMessage if [[ "$MODE" == "list-message" ]]; then if GetSubscribersList; then ReEncryptAndSendListMessage else return_code=1 fi elif [[ "$MODE" == "admin-non-interactive" ]]; then EmailListAdministration 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" && "$REQUIRE_SIGNATURE" == "1" ]]; then GetSenderAddress if [[ -n $(echo $LIST_ADMIN) || -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" && "$REQUIRE_SIGNATURE" == "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 elif [[ "$SIGNATURE_MADE_BY_SENDER" != "1" && "$REQUIRE_SIGNATURE" == "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 sent by the person who signed it." 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 # if bounce processing is enabled, continue if [[ "$SILENTLY_DISCARD_INVALID_MESSAGES" != "1" ]]; then GetSenderAddress if [[ -n "$SENDER_ADDRESS" ]]; then # the anti-replay mechanism detected a repeated message MESSAGE_BODY="\ It was not possible to process this message. This list is configured to discard replayed messages as an attack protection measure. It looks like your message has been sent to the list before and so it was discarded. Contact the list administrator if you have any questions." ComposeAndSendBounceMessage 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 local answer admin invalid method local last_char digits_only # UTF-8 is avoided in DETAILS echo "Firma will ask you some questions to setup your list." echo "Please don't use UTF-8 characters." read -rep " List keyring location: ($LIST_PATH) " LIST_HOMEDIR LIST_HOMEDIR=${LIST_HOMEDIR:-"$LIST_PATH"} if [[ -d "$LIST_HOMEDIR" ]]; then echo "Cannot create list $LIST_NAME: List already exists at $LIST_HOMEDIR" return_code=1 else echo "Creating folder $LIST_HOMEDIR..." mkdir -p $LIST_HOMEDIR if [[ -d "$LIST_HOMEDIR" ]]; then # list address while true; do read -rep " List email address or 'quit' to exit: " LIST_ADDRESS if [[ "$LIST_ADDRESS" == "quit" ]]; then echo "Deleting folder $LIST_HOMEDIR..." rm -rf $LIST_HOMEDIR echo "List creation aborted." return_code=1 break elif CheckValidEmail $LIST_ADDRESS; then break elif [[ -n "$LIST_ADDRESS" ]]; then echo " Invalid email address: $LIST_ADDRESS" fi done # admin emails if [[ "$return_code" == "0" ]]; then while true; do read -rep " List administrator(s) email address(es) (space delimited) or 'quit' to exit: " LIST_ADMIN if [[ "$LIST_ADMIN" == "quit" ]]; then echo "Deleting folder $LIST_HOMEDIR..." rm -rf $LIST_HOMEDIR echo "List creation aborted." return_code=1 break elif [[ -n "$LIST_ADMIN" ]]; then for admin in $LIST_ADMIN; do if ! CheckValidEmail $admin; then invalid="$(echo $invalid $admin | sed -e 's/ / /')" fi done if [[ -n "$invalid" ]]; then echo " Invalid email address: $invalid" invalid="" else break fi fi done fi # list description, passphrase and key size if [[ "$return_code" == "0" ]]; then read -rep " List description (optional): " KEY_DESCRIPTION if [[ ! -z "$KEY_DESCRIPTION" ]]; then KEY_DESCRIPTION="Name-Real: $KEY_DESCRIPTION" fi while true; do read -rep " Automatically create a passphrase for the list pubkey? (Y/n) " answer answer="$(echo $answer | tr '[:lower:]' '[:upper:]')" if [[ -z "$answer" || "$answer" == "Y" || "$answer" == "YES" ]]; then PASSPHRASE="$(RandomString 62)" while ! CheckPassphrase; do PASSPHRASE="$(RandomString 62)" done break elif [[ "$answer" == "N" || "$answer" == "NO" ]]; then read -resp " Passphrase to protect the list's secret key (you'll type it once): " PASSPHRASE while ! CheckPassphrase; do echo "" read -resp " Passphrase doesn't fit all the requirements, please choose another: " PASSPHRASE done break else echo " Please answer either yes or no." fi done while true; do echo " Please choose a key size:" echo " 1 - 1024" echo " 2 - 2048" echo " 3 - 4096 (default)" read -rep " Please choose a key size or 'quit' to exit: " answer answer="$(echo $answer | tr '[:lower:]' '[:upper:]')" if [[ "$answer" == "QUIT" ]]; then echo "Deleting folder $LIST_HOMEDIR..." rm -rf $LIST_HOMEDIR echo "List creation aborted." return_code=1 break elif [[ "$answer" == "1" || "$answer" == "1024" ]]; then KEY_SIZE="1024" break elif [[ "$answer" == "2" || "$answer" == "2048" ]]; then KEY_SIZE="2048" break elif [[ -z "$answer" || "$answer" == "3" || "$answer" == "4096" ]]; then KEY_SIZE="4096" break else echo " Invalid answer." fi done fi # key expiration if [[ "$return_code" == "0" ]]; then echo " Choose a key validity:" echo " 0 = key does not expire (default)" echo " = key expires in n days" echo " w = key expires in n weeks" echo " m = key expires in n months" echo " y = key expires in n years" while true; do read -rep " Please enter the key expiration time or 'quit' to exit: " KEY_EXPIRATION KEY_EXPIRATION="$(echo $KEY_EXPIRATION | tr '[:upper:]' '[:lower:]')" last_char="$(echo "$KEY_EXPIRATION" | grep -o '[wmy]$')" digits_only="$(echo "$KEY_EXPIRATION" | sed -e "s/${last_char}$//")" if [[ -z "$KEY_EXPIRATION" ]]; then KEY_EXPIRATION="0" break elif [[ "$KEY_EXPIRATION" == "quit" ]]; then echo "Deleting folder $LIST_HOMEDIR..." rm -rf $LIST_HOMEDIR echo "List creation aborted." return_code=1 break elif [[ -z "$(echo $digits_only | sed -e 's/[0-9]//g')" ]]; then break else echo " Invalid key expiration time." fi done fi # config file creation if [[ "$return_code" == "0" ]]; then echo "Creating your config..." touch $LIST_CONFIG_FILE 2> /dev/null chmod 600 $LIST_CONFIG_FILE 2> /dev/null chown $FIRMA_USER:$FIRMA_GROUP $LIST_CONFIG_FILE 2> /dev/null 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 echo -e "KEY_SIZE='$KEY_SIZE'\nKEY_DESCRIPTION='$KEY_DESCRIPTION'" >> $LIST_CONFIG_FILE echo "Now generating your keyring..." $GPG --gen-key < /dev/null echo "Your list was created. Now check its configuration at $LIST_CONFIG_FILE." echo "To see a list of optional config parameters, type firma --help config." fi fi else echo "Could not create list homedir $LIST_HOMEDIR." return_code=1 fi fi # list creation should be atomic if [[ "$return_code" == "0" ]]; then echo "List creation complete." fi return $return_code } function AdminHelp { #------------------------------------------------------------- # display help on admin commands # # parameter(s): none # depends on function(s): none # returns: 0 #------------------------------------------------------------- # this will be printed to STDOUT, so no indentation here echo " quit quit the admin prompt help show this help list show list subscribers listinfo show list information config list configuration info EMAIL-ADDRESS show info of a given subscriber sendkey SUBSCRIBER send list pubkey to subscriber sub|subscribe [..] subscribe users ('subscribe help' for options) unsub|unsubscribe EMAIL-ADDRESS unsubscribe an email from the list use EMAIL-ADDRESS use the given address for message delivery instead of the primary address on key " } function ListAdministration { #------------------------------------------------------------- # process administrative tasks # # parameter(s): task to be performed (plus its argument(s)) # depends on function(s): ChooseUid, CheckValidEmail, UbsubscribeUser # SubscribeUsers, SendListPubkey # returns: 0 if task is executed successfully, # 1 if task can't be executed (command not found, too many/missing arguments, etc.), # 3 if a quit command is entered #------------------------------------------------------------- local -i return_code=0 local subscribers # NOTE: when adding a new admin command, dont forget to # - add a summary on AdminHelp function # - if needed, add a "help" option to show more detailed help case $# in 1) case $1 in help) AdminHelp ;; quit) return_code=3 ;; use) AdminLog "$1: missing arguments (try \"help\")" return_code=1 ;; unsub|unsubscribe) AdminLog "$1: missing arguments (try \"help\")" return_code=1 ;; list) GetSubscribersList for subscriber in $SUBSCRIBERS_LIST; do AdminLog " $subscriber" done ;; sub|subscribe) AdminLog "$1: missing arguments (try \"$1 help\")" return_code=1 ;; sendkey) AdminLog "$1: missing arguments (try \"$1 help\")." return_code=1 ;; info) AdminLog "$1: missing arguments (try \"help\")." return_code=1 ;; listinfo) GetSubscribersInfo $LIST_ADDRESS ;; config) AdminLog "$1: missing arguments (try \"$1 help\")." return_code=1 ;; *) AdminLog "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 AdminLog "$1: invalid argument -- $2 (try \"help\")" return_code=1 fi ;; unsub|unsubscribe) UnsubscribeUser $2 return_code=$? ;; sub|subscribe) SubscribeUsers $2 return_code=$? ;; sendkey) SendListPubkey $2 return_code=$? ;; info) GetSubscribersInfo $2 return_code=$? ;; listinfo) AdminLog "$1: too many arguments -- $@ (try \"help\")" return_code=1 ;; config) if [[ "$2" == "help" ]]; then ConfigHelp return_code=$? else AdminLog "$1: too many arguments -- $@ (try \"$1 help\")" return_code=1 fi ;; help|quit) AdminLog "$1: too many arguments -- $@ (try \"help\")" return_code=1 ;; *) AdminLog "Command not found -- $1 (try \"help\")" return_code=1 ;; esac ;; *) case $1 in help|quit|use) AdminLog "$1: too many arguments -- $@ (try \"help\")" return_code=1 ;; sub|subscribe) shift SubscribeUsers $* return_code=$? ;; sendkey) shift SendListPubkey $* return_code=$? ;; info) shift GetSubscribersInfo $* return_code=$? ;; listinfo) AdminLog "$1: too many arguments -- $@ (try \"help\")" return_code=1 ;; *) AdminLog "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 AdminLog "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 AdminLog "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 AdminLog "use: \"$1\" is listed in more than one UID on this keyring." AdminLog "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 if [[ "$return_code" == "0" || "$?" == "0" ]]; then AdminLog "use: $1 chosen for message delivery. $(($uid_count - 1)) UID(s) deleted from public key ${keyid:32}." else return_code=1 fi FixListOwnership return $return_code } function CheckPermission { #------------------------------------------------------------- # check if file has correct permissions (600) and also #+if the file is owned by $FIRMA_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! Wrong permission for file $file" return 1 fi if [[ $(ls -ld $file | cut -d " " -f 3) != "$FIRMA_USER" ]]; then LogMessage "WARNING: Configuration files must be owned by $FIRMA_USER! Wrong ownership for file $file" return 1 fi return 0 } function CheckListPermissions { #------------------------------------------------------------- # check if list files has correct permissions (600) and also #+if the files are owned by $FIRMA_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 $FIRMA_USER:$FIRMA_GROUP if [[ -n "$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 2> /dev/null chown $FIRMA_USER:$FIRMA_GROUP $folder/$file 2> /dev/null 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 #------------------------------------------------------------- local local_part='[[:alnum:]][[:alnum:]._+-]*[[:alnum:]]' local domain='[[:alnum:]][[:alnum:].-]*[[:alnum:]]' local tld='[[:alpha:]]\{2,6\}' if ! echo "$1" | grep -q "^${local_part}@${domain}\.${tld}$"; then return 1 else return 0 fi } function UnsubscribeUser { #------------------------------------------------------------- # unsubscribe a user and remove its pubkey from #+the list keyring # # 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, etc.) #------------------------------------------------------------- local key local -i return_code=0 local keyid="$($GPG_LIST_KEYS --with-fingerprint $1 2> /dev/null | grep ^fpr | cut -d : -f 10)" # check if its a valid email if ! CheckValidEmail $1; then AdminLog "unsub: \"$1\" is not an email address." return_code=1 # check if user is trying to unsubscribe the list key elif [[ "$1" == "$LIST_ADDRESS" ]]; then AdminLog "unsub: can't delete the list pubkey." return_code=1 # check if supplied address is associated with a public key elif [[ -z "$($GPG_LIST_KEYS --fixed-list-mode "<$1>" 2> /dev/null | grep -v '^tru:')" ]]; then AdminLog "unsub: \"$1\" is not associated with any public key on this keyring." return_code=1 else for key in $keyid; do $GPG --batch --delete-key --yes $key if [[ "$?" == "0" ]]; then AdminLog "deleted key id $key for $1" # now just update the trust db $GPG_LIST_KEYS &> /dev/null else AdminLog "unsub: error deleting key id $key for $1" return_code=1 fi done fi FixListOwnership return $return_code } function LogMessage { #------------------------------------------------------------- # write a log message to STDOUT or to syslog # # parameter(s): string # depends on function(s): none # returns: 0 #------------------------------------------------------------- local error_message="$*" local line if [[ "$LOG_TO_SYSLOG" == "1" ]]; then echo "$error_message" | $LOGGER_BINARY -p "$SYSLOG_PRIORITY" -t "$BASENAME" else echo "$error_message" | while read line; do echo >&2 "$BASENAME: $line" done fi return 0 } function SubscribeUsers { #------------------------------------------------------------- # subscribe users to the list importing their pubkeys # # parameter(s): $1: help, stdin, keyserver or file # $2: where to fetch the pubkeys # $3: keyid (keyserver only) # depends on function(s): none # returns: 0 on success # 1 on failure #------------------------------------------------------------- local -i return_code=0 local keyserver local method if [[ "$1" == "help" ]]; then AdminLog " help show this help stdin waits for key material from stdin (interactive mode only) file import pubkeys from file (interactive mode only) keyserver [server-address] import from (default keyserver: $KEYSERVER) " elif [[ "$1" == "stdin" ]]; then if [[ "$MODE" == "admin-interactive" ]]; then echo "Please enter the key material here, finishing with Ctrl-D sequence..." $GPG_NOBATCH --import return_code=$? if [[ "$return_code" == "0" ]]; then AdminLog "subscription: success" fi else AdminLog "subscribe: stdin option only valid in the interactive (command-line) mode" return_code=1 fi elif [[ "$1" == "file" ]]; then if [[ -n "$2" ]]; then if [[ -f "$2" ]]; then $GPG --import < $2 return_code=$? if [[ "$return_code" == "0" ]]; then AdminLog "subscription: success" fi else echo >&2 "subscribe: cant add subscribers from $1: no such file or directory" return_code=1 fi else echo >&2 "subscribe: missing parameters: subscribe file requires a file name" return_code=1 fi elif [[ "$1" == "keyserver" ]]; then if [[ -n "$2" ]]; then if [[ -z "$3" ]]; then keyserver="$KEYSERVER" else if ! CheckValidEmail $2; then keyserver="$2" shift else keyserver="$KEYSERVER" fi fi if CheckValidEmail $2; then method="--search-keys" else method="--recv-keys" fi if [[ "$return_code" == "0" ]]; then shift $GPG_NOBATCH --keyserver $keyserver $method $* return_code=$? if [[ "$return_code" == "0" ]]; then AdminLog "subscription: success" fi fi else AdminLog "subscribe: missing parameters: type subscribe help" return_code=1 fi else AdminLog "subscribe: wrong option: type subscribe help" return_code=1 fi FixListOwnership return $return_code } function SendListPubkey { #------------------------------------------------------------- # send list pubkey to a given subscriber list # # parameter(s): subscribers' emails # depends on function(s): GetMessage, GetSenderAddress, AssembleMessage # returns: 0 on success # 1 on failure #------------------------------------------------------------- local key local keys local keyid local keyboundary if [[ "$1" == "help" ]]; then AdminLog "usage: sendkey [all|email|help]" AdminLog "supported arguments: all (for all subscribers) or a space-separated subscriber emails." return 0 elif [[ "$1" == "all" ]]; then GetSubscribersList keys="$SUBSCRIBERS_LIST" else keys="$*" fi for key in $keys; do keyid="$($GPG_LIST_KEYS --with-fingerprint $1 2> /dev/null | grep ^fpr | cut -d : -f 10)" if [[ -z "$key" ]]; then AdminLog "sendkey: missing argument: subscriber email address." return 1 elif ! CheckValidEmail $key; then AdminLog "sendkey: \"$key\" is not an email address." return 1 elif [[ -z "$($GPG_LIST_KEYS --fixed-list-mode "<$key>" 2> /dev/null | grep -v '^tru:')" ]]; then # check if supplied address is associated with a public key AdminLog "sendkey: \"$key\" is not associated with any public key on this keyring." return 1 fi RECIPIENTS="$key" SUBJECT="mailing list public key" keyboundary="$(RandomString 15)" # this is the body of the message to be sent, so no indentation here MESSAGE_BODY="$($GPG --armor --export $LIST_ADDRESS)" MESSAGE_BODY="\ Content-Type: multipart/mixed; boundary=\"$keyboundary\" Content-Disposition: inline --$keyboundary Content-Type: application/pgp-keys Content-Disposition: attachment; filename=${LIST_NAME}_pubkey.asc Content-Description: PGP Key for $LIST_ADDRESS $MESSAGE_BODY --${keyboundary}--" # compose the message MimeWrapMessage # send message echo "$MESSAGE" | $MAIL_AGENT $MAIL_AGENT_ARGS $RECIPIENTS done AdminLog "List pubkey sent to $keys." FixListOwnership } function GetSubscribersInfo { #------------------------------------------------------------- # get info on a subscriber pubkey # # parameter(s): subscribers' emails # depends on function(s): none # returns: 0 on success # 1 on failure #------------------------------------------------------------- local key local keys local keyid local output if [[ "$1" == "help" ]]; then AdminLog "usage: info [all|emails|help]" AdminLog "supported arguments: all (for all subscribers) or a space-separated subscribers' emails." return 0 elif [[ "$1" == "all" ]]; then GetSubscribersList keys="$SUBSCRIBERS_LIST" else keys="$*" fi for key in $keys; do keyid="$($GPG_LIST_KEYS --with-fingerprint $1 2> /dev/null | grep ^fpr | cut -d : -f 10)" if [[ -n "$keyid" ]]; then output="$($GPG --fingeprint $key)" AdminLog "$output" fi done FixListOwnership return $? } function FixListOwnership { #------------------------------------------------------------- # fix list ownership # # parameter(s): none # depends on function(s): none # returns: 0 on success # 1 on failure #------------------------------------------------------------- if [[ -d "$LIST_PATH" ]]; then chown -R $FIRMA_USER:$FIRMA_GROUP $LIST_PATH 2> /dev/null fi return $? } function RandomString { #------------------------------------------------------------- # print a random string #+got it from http://funcoeszz.net/ # # parameter(s): string size (max 62) # depends on function(s): none # returns: 0 # 1 if string size is greater than 62 #------------------------------------------------------------- local n alpha="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,;:?!" n="$(( 10#$(echo "$1" | tr -dc '[:digit:]') ))" if [[ "$n" == "0" ]]; then n="6" fi if [[ "$n" -gt "62" ]]; then return 1 fi while [[ "$n" != "0" ]]; do n="$((n-1))" ; pos="$((RANDOM%${#alpha}+1))" echo -n "$alpha" | sed "s/\(.\)\{$pos\}.*/\1/" alpha="$(echo $alpha | sed "s/.//$pos")" done | tr -d '\012' ; echo return 0 } function AdminLog { #------------------------------------------------------------- # check whether admin is made via command line #+or email and then log a message according to the #+display mode # # parameter(s): string # depends on function(s): none # returns: 0 #------------------------------------------------------------- if [[ "$MODE" == "admin-interactive" ]]; then echo >&2 "$*" else echo "$*" fi } function EmailListAdministration { #------------------------------------------------------------- # parse and execute admin tasks via email # # parameter(s): none # depends on function(s): ProcessMessage should be called first # returns: 0 on success :) # 1 on failure :/ #------------------------------------------------------------- local sender found local command found="0" for sender in $LIST_ADMIN; do if [[ "$sender" == "$SENDER_ADDRESS" ]]; then found="1" break fi done if [[ "$found" == "1" ]]; then # message was sent by an admin #+then, parse and process admin tasks MESSAGE_BODY="$(echo -e "$DECRYPTED_MESSAGE" | while read command; do if [[ -n "$command" ]] && echo $command | grep -q -v -e "^Content"; then AdminLog "Command> $command" ListAdministration $command fi done)" # send a message back to the administrator RECIPIENTS="$SENDER_ADDRESS" SUBJECT="admin request results" CreateMessageBodyPart MimeWrapMessage echo "$MESSAGE" | $MAIL_AGENT $MAIL_AGENT_ARGS $SENDER_ADDRESS else # message was sent by a normal subscriber # 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 sent by a list administrator." ComposeAndSendBounceMessage fi return $? } function AllowMessageProcessing { #------------------------------------------------------------- # check if the message has sufficient rights to be processed # # parameter(s): none # depends on function(s): ParseGpgDecryptStderr # returns: 0 if message has rights to be processed # 1 if not #------------------------------------------------------------- local -i return_code=0 if [[ "$MODE" == "admin-non-interactive" ]]; then REQUIRE_SIGNATURE="1" fi if [[ "$ENCRYPTED_TO_LIST" == "1" ]]; then if [[ "$REQUIRE_SIGNATURE" == "1" ]]; then if [[ "$GOOD_SIGNATURE" == "1" && "$SIGNATURE_MADE_BY_SENDER" == "1" ]]; then return_code=0 else return_code=1 fi else return_code=0 fi else return_code=1 fi return $return_code } function MimeWrapMessage { #------------------------------------------------------------- # MIME wrap message to be sent, adding needed headers and body parts # # parameter(s): none # depends on variable(s): RECIPIENTS, SUBJECT, GPG_MESSAGE # # returns: 0 #------------------------------------------------------------- local boundary boundary="$(RandomString 15)" # these are the headers of the message to be sent, so no indentation here MESSAGE_HEADERS="\ From: $LIST_REQUEST_ADDRESS To: ${RECIPIENTS} Reply-To: $LIST_REQUEST_ADDRESS Subject: ${SUBJECT_PREFIX}${SUBJECT} MIME-Version: 1.0 Content-Type: multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=\"${boundary}\" Content-Disposition: inline" GPG_MESSAGE="$(echo -e "${PASSPHRASE}\n${MESSAGE_BODY}" | $GPG_ENCRYPT --recipient $RECIPIENTS)" # this is the body of the message to be sent, so no indentation here MESSAGE_BODY="\ This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156) --$boundary Content-Type: application/pgp-encrypted Content-Description: PGP/MIME version identification Version: 1 --$boundary Content-Type: application/octet-stream; name=\"encrypted.asc\" Content-Disposition: inline; filename=\"encrypted.asc\" ${GPG_MESSAGE} --${boundary}--" # assemble entire message MESSAGE="\ ${MESSAGE_HEADERS} ${MESSAGE_BODY}" } function CreateMessageBodyPart { #------------------------------------------------------------- # create a message body part # # parameter(s): none # depends on variable(s): MESSAGE_BODY # # returns: 0 #------------------------------------------------------------- # this is the body of the message to be sent, so no indentation here MESSAGE_BODY="\ Content-Type: text/plain; charset=iso-8859-1 Content-Disposition: inline $MESSAGE_BODY" } function EvalConfigParameter { #------------------------------------------------------------- # eval parameters from a config file # # parameter(s): # depends on function(s): none # returns: 0 on success # 1 if config file not found or missing parameter #------------------------------------------------------------- if [[ "$#" != "2" ]]; then echo "WARNING: missing parameters on EvalConfigParameters." return 1 elif [[ ! -f "$1" ]]; then echo "WARNING: file not found: $1" return 1 fi echo "$(grep "^$2=" $1 | sed -e "s/^$2='//" -e "s/'$//" | tail -n 1)" } function SourceFirmaConfig { #------------------------------------------------------------- # load firma.conf and set up global variables # # parameter(s): none for evaluation, help to show all config parameters # depends on function(s): none # returns: 0 #------------------------------------------------------------- [[ "$1" == "help" ]] && echo -e "\nMandatory global firma config parameters\n" [[ "$1" == "help" ]] && echo -e "\tGPG_BINARY= path to the GnuPG binary." || \ GPG_BINARY="$(EvalConfigParameter $FIRMA_CONFIG_FILE GPG_BINARY)" [[ "$1" == "help" ]] && echo -e "\tMAIL_AGENT= path to the mail transport agent to be used (e.g., sendmail)." || \ MAIL_AGENT="$(EvalConfigParameter $FIRMA_CONFIG_FILE MAIL_AGENT)" [[ "$1" == "help" ]] && echo -e "\tMAIL_AGENT_ARGS= command-line arguments to be passed to the command above." || \ MAIL_AGENT_ARGS="$(EvalConfigParameter $FIRMA_CONFIG_FILE MAIL_AGENT_ARGS)" [[ "$1" == "help" ]] && echo -e "\tLISTS_DIR= path to the mailing lists directory." || \ LISTS_DIR="$(EvalConfigParameter $FIRMA_CONFIG_FILE LISTS_DIR)" [[ "$1" == "help" ]] && echo -e "\nOptional global firma config parameters\n" [[ "$1" == "help" ]] && echo -e "\tFIRMA_USER= user that runs firma (usually the same as your MTA user); \t defaults to \"nobody\"; you can also specify this parameter \t in each mailing list config file if you plan to have one \t user per mailing list." || \ FIRMA_USER="$(EvalConfigParameter $FIRMA_CONFIG_FILE FIRMA_USER)" [[ "$1" == "help" ]] && echo -e "\tFIRMA_GROUP= group that runs firma (usually the same as your MTA group); \t defaults to \"nobody\"; you can also specify this parameter \t in each mailing list config file if you plan to have one \t group per mailing list." || \ FIRMA_GROUP="$(EvalConfigParameter $FIRMA_CONFIG_FILE FIRMA_GROUP)" [[ "$1" == "help" ]] && echo -e "\tLOG_TO_SYSLOG= set to "1" to log errors and warnings to syslog, else firma \t will print errors to STDERR." || \ LOG_TO_SYSLOG="$(EvalConfigParameter $FIRMA_CONFIG_FILE LOG_TO_SYSLOG)" [[ "$1" == "help" ]] && echo -e "\tLOGGER_BINARY= if logging to syslog, set the path to logger's binary." || \ LOGGER_BINARY="$(EvalConfigParameter $FIRMA_CONFIG_FILE LOGGER_BINARY)" [[ "$1" == "help" ]] && echo -e "\tSYSLOG_PRIORITY= if logging to syslog, set a priority for the error messages \t (defaults to "user.err")." || \ SYSLOG_PRIORITY="$(EvalConfigParameter $FIRMA_CONFIG_FILE SYSLOG_PRIORITY)" [[ "$1" == "help" ]] && echo -e "\tUSE_GPG_HIDDEN_RECIPIENT_OPTION= set to '1' to use GnuPG's --hidden-recipient \t option, available from version 1.4.0 onwards \t (try 'man gpg' for more information)." || \ USE_GPG_HIDDEN_RECIPIENT_OPTION="$(EvalConfigParameter $FIRMA_CONFIG_FILE USE_GPG_HIDDEN_RECIPIENT_OPTION)" [[ "$1" == "help" ]] && echo -e "\tREMOVE_THESE_HEADERS_ON_ALL_LISTS= headers that should be stripped from list \t messages on all lists running under firma \t (space separated case-insensitive entries) \t (may include regexps (e.g., X-.*)." || \ REMOVE_THESE_HEADERS_ON_ALL_LISTS="$(EvalConfigParameter $FIRMA_CONFIG_FILE REMOVE_THESE_HEADERS_ON_ALL_LISTS)" [[ "$1" == "help" ]] && echo -e "\tKEYSERVER= default keyserver to import/export keys \t (defaults to keys.indymedia.org)." || \ KEYSERVER="$(EvalConfigParameter $FIRMA_CONFIG_FILE KEYSERVER)" } function SourceListConfig { #------------------------------------------------------------- # load list.conf and set up global variables # # parameter(s): none for evaluation, help to show all config parameters # depends on function(s): none # returns: 0 #------------------------------------------------------------- local firma_user firma_group keyserver [[ "$1" == "help" ]] && echo -e "\nMandatory list config parameters\n" [[ "$1" == "help" ]] && echo -e "\tLIST_ADDRESS= list's email address." || \ LIST_ADDRESS="$(EvalConfigParameter $LIST_CONFIG_FILE LIST_ADDRESS)" [[ "$1" == "help" ]] && echo -e "\tLIST_REQUEST_ADDRESS= list's email address for administrative \t requests (defaults to listname-request@domain." || \ LIST_REQUEST_ADDRESS="$(EvalConfigParameter $LIST_CONFIG_FILE LIST_REQUEST_ADDRESS)" [[ "$1" == "help" ]] && echo -e "\tLIST_ADMIN= list's administrators email addresses (space separated)." || \ LIST_ADMIN="$(EvalConfigParameter $LIST_CONFIG_FILE LIST_ADMIN)" [[ "$1" == "help" ]] && echo -e "\tLIST_HOMEDIR= list's GnuPG homedir, where the list's keyrings are located." || \ LIST_HOMEDIR="$(EvalConfigParameter $LIST_CONFIG_FILE LIST_HOMEDIR)" [[ "$1" == "help" ]] && echo -e "\tFIRMA_USER= user that runs firma (usually the same as your MTA user); \t defaults to \"nobody\"; you can also specify this parameter \t in each mailing list config file if you plan to have one \t user per mailing list." || \ firma_user="$(EvalConfigParameter $LIST_CONFIG_FILE FIRMA_USER)" [[ -n "$firma_user" ]] && FIRMA_USER="$firma_user" [[ "$1" == "help" ]] && echo -e "\tFIRMA_GROUP= group that runs firma (usually the same as your MTA group); \t defaults to \"nobody\"; you can also specify this parameter \t in each mailing list config file if you plan to have one \t group per mailing list." || \ firma_group="$(EvalConfigParameter $LIST_CONFIG_FILE FIRMA_GROUP)" [[ -n "$firma_group" ]] && FIRMA_GROUP="$firma_group" [[ "$1" == "help" ]] && echo -e "\tPASSPHRASE= passphrase for the list's private keyring\n \tNOTE: The passphrase _has_ to be enclosed in single quotes and _cannot_ \tcontain any additional single quote as part of itself. It has to be at least \t25 characters long, combining numbers, upper and lower case letters and at \tleast 1 special characters. Also, no character can be sequentially repeated \tmore than 4 times." || \ PASSPHRASE="$(EvalConfigParameter $LIST_CONFIG_FILE PASSPHRASE)" [[ "$1" == "help" ]] && echo -e "\nOptional list config parameters\n" [[ "$1" == "help" ]] && echo -e "\tSUBJECT_PREFIX= prefix to be included in the subject of list messages." || \ SUBJECT_PREFIX="$(EvalConfigParameter $LIST_CONFIG_FILE SUBJECT_PREFIX)" [[ "$1" == "help" ]] && echo -e "\tREMOVE_THESE_HEADERS= headers that should be stripped from list messages \t (space separated case-insensitive entries) \t (may include regexps (e.g., X-.*)." || \ REMOVE_THESE_HEADERS="$(EvalConfigParameter $LIST_CONFIG_FILE REMOVE_THESE_HEADERS)" [[ "$1" == "help" ]] && echo -e "\tREPLIES_SHOULD_GO_TO_LIST= set to '1' to add a Reply-To header containing the list address." || \ REPLIES_SHOULD_GO_TO_LIST="$(EvalConfigParameter $LIST_CONFIG_FILE REPLIES_SHOULD_GO_TO_LIST)" [[ "$1" == "help" ]] && echo -e "\tHIDE_SENDER= set to '1' to add replace the sender address with list address on list messages." || \ HIDE_SENDER="$(EvalConfigParameter $LIST_CONFIG_FILE HIDE_SENDER)" [[ "$1" == "help" ]] && echo -e "\tSILENTLY_DISCARD_INVALID_MESSAGES= set to '1' to silently discard invalid \t messages (message not signed/encrypted, \t sender not subscribed to the list, etc.) \t instead of sending bounces back to sender." || \ SILENTLY_DISCARD_INVALID_MESSAGES="$(EvalConfigParameter $LIST_CONFIG_FILE SILENTLY_DISCARD_INVALID_MESSAGES)" [[ "$1" == "help" ]] && echo -e "\tKEYSERVER= default keyserver to import/export keys \t (defaults to keys.indymedia.org)." || \ keyserver="$(EvalConfigParameter $LIST_CONFIG_FILE KEYSERVER)" [[ -n "$keyserver" ]] && KEYSERVER="$keyserver" [[ "$1" == "help" ]] && echo -e "\tREQUIRE_SIGNATURE= whether messages sent to the list should be (1) or dont \t need to be (0) signed to be processed; defaults to '1'; \t this doesnt affect the way email administration works, \t when signature is mandatory." || \ REQUIRE_SIGNATURE="$(EvalConfigParameter $LIST_CONFIG_FILE REQUIRE_SIGNATURE)" [[ "$1" == "help" ]] && echo -e "\tDELIVERY_RANDOMIZATION= if non-zero, set a random delay between 0 and N seconds \t between each messsage delivery; if you run firma with \t a TLS-enabled MTA and mostly of the list messages are \t sent to others TLS-enabled MTAs, then this option will \t make harder to a sniffer detect the traffic of you mailing \t list, specially if your MTA already sends a lot of messages \t or if you're going to have a lot of encrypted mailing lists, \t all randomizing its delivery." || \ DELIVERY_RANDOMIZATION="$(EvalConfigParameter $LIST_CONFIG_FILE DELIVERY_RANDOMIZATION)" [[ "$1" == "help" ]] && echo -e "\tREPLAY_PROTECTION= when set to '1', stores sha1sums \t of the last REPLAY_COUNT received messages; then, \t if some message with an already stored sha1sum, then \t its bounced back to the sender and considered as an attempt \t of replay attack." || \ REPLAY_PROTECTION="$(EvalConfigParameter $LIST_CONFIG_FILE REPLAY_PROTECTION)" [[ "$1" == "help" ]] && echo -e "\tREPLAY_COUNT= number of messages to store sha1sums; \t defaults to 150 and only used when \t REPLAY_PROTECTION is set to '1'." || \ REPLAY_COUNT="$(EvalConfigParameter $LIST_CONFIG_FILE REPLAY_COUNT)" [[ "$1" == "help" ]] && echo -e "\tREPLAY_FILE= file to store sha1sums of messages; \t only used when REPLAY_PROTECTION is set to '1'; \t defaults to \"/var/log/firma/replay.db\"." || \ REPLAY_FILE="$(EvalConfigParameter $LIST_CONFIG_FILE REPLAY_FILE)" } function ConfigHelp { #------------------------------------------------------------- # display help on configuration file parameters # # parameter(s): none # depends on function(s): SourceFirmaConfig, SourceListConfig # returns: 0 #------------------------------------------------------------- echo "All firma parameters are passed through two different configuration files:" echo "firma.conf, containing general parameters needed to run the script, and a list" echo "specific file, containing its address, administrator(s), etc. In both files" echo "you should enter PARAMETER='value' (with value between single quotes, without" echo "spaces before or after the equal sign and nothing after the second quote)." SourceFirmaConfig help SourceListConfig help } function DeliveryRandomization { #------------------------------------------------------------- # sleep according $DELIVERY_RANDOMIZATION # # parameter(s): none # depends on function(s): none # returns: 0 #------------------------------------------------------------- local n if [[ "$DELIVERY_RANDOMIZATION" != "0" ]]; then n="$RANDOM" let "n %= $DELIVERY_RANDOMIZATION" sleep $n fi } function ReplayProtectionFlush { #------------------------------------------------------------- # flushes the replay database file # # parameter(s): none # depends on function(s): none # returns: 0 #------------------------------------------------------------- if [[ "$REPLAY_PROTECTION" == "1" ]]; then if [[ -f "$REPLAY_FILE" ]]; then if [[ "$(wc -l $REPLAY_FILE | cut -d " " -f 1)" -gt "$REPLAY_COUNT" ]]; then sed -i -e '1d' $REPLAY_FILE fi else touch $REPLAY_FILE 2> /dev/null chown $FIRMA_USER:$FIRMA_GROUP $REPLAY_FILE 2> /dev/null chmod 600 $REPLAY_FILE 2> /dev/null fi fi return 0 } function ReplayProtectionCheck { #------------------------------------------------------------- # check if message was already received and stores it #+in the database # # parameter(s): GetGpgMessage, ReplayProtectionFlush # depends on function(s): none # returns: 0 if message's sha1sum is not in replay database # 1 if message's sha1sum is in the database #------------------------------------------------------------- local -i return_code=0 local sha1 if [[ "$REPLAY_PROTECTION" == "1" && -n "$ORIG_GPG_MESSAGE" ]]; then ReplayProtectionFlush sha1="$(echo "$ORIG_GPG_MESSAGE" | sha1sum | cut -d " " -f 1)" if grep -q "^$sha1$" $REPLAY_FILE; then sed -i -e "/^$sha1$/d" $REPLAY_FILE return_code=1 fi echo "$sha1" >> $REPLAY_FILE fi return $return_code } #------------------------------------------------------------- # main() #------------------------------------------------------------- # hardcode path to firma.conf, firma version and program name declare -r \ FIRMA_CONFIG_FILE="/var/lib/firma/firma.conf" \ VERSION="0.4-git" \ BASENAME="$(basename $0)" # set environmental variables and options export LANG=en_US umask 0077 # set initial exit code EXIT_CODE=0 # define the number of command line arguments, the option #+being run and its argument, if any declare -r \ NUM_OF_ARGS="$#" \ OPTION="$1" \ ARG="$2" # check command line arguments for errors #+(missing arguments, invalid option, etc) case "$NUM_OF_ARGS" in 0) echo >&2 "$BASENAME: missing arguments" Usage EXIT_CODE=1 ;; 1) case "$OPTION" in # valid short option -h|-v) ;; # valid long option --help|--version) ;; # valid short option called without its required argument -a|-c|-e|-p) echo >&2 "$BASENAME: missing arguments" Usage EXIT_CODE=1 ;; # valid long option called without its required argument --admin-task|--create-newlist|--email-admin-task|--process-message) echo >&2 "$BASENAME: missing arguments" Usage EXIT_CODE=1 ;; # invalid option *) echo >&2 "$BASENAME: invalid option -- $OPTION" Usage EXIT_CODE=1 ;; esac ;; 2) case "$OPTION" in # valid short option -a|-c|-e|-p|-h) ;; # valid long option --admin-task|--create-newlist|--email-admin-task|--process-message|--help) ;; # valid short option called with too many arguments -v) echo >&2 "$BASENAME: too many arguments -- $@" Usage EXIT_CODE=1 ;; # valid long option called with too many arguments --version) echo >&2 "$BASENAME: too many arguments -- $@" Usage EXIT_CODE=1 ;; # invalid option *) echo >&2 "$BASENAME: invalid option -- $OPTION" Usage EXIT_CODE=1 ;; esac ;; *) case "$OPTION" in # valid short option called with too many arguments -h|-v|-a|-c|-e|-p) echo >&2 "$BASENAME: too many arguments -- $@" Usage EXIT_CODE=1 ;; # valid long option called with too many arguments --help|--version|--admin-task|--create-newlist|--email-admin-task|--process-message) echo >&2 "$BASENAME: too many arguments -- $@" Usage EXIT_CODE=1 ;; # invalid option *) echo >&2 "$BASENAME: invalid option -- $OPTION" Usage EXIT_CODE=1 ;; esac ;; esac # parse valid command line arguments if [[ "$EXIT_CODE" == "0" ]]; then case "$NUM_OF_ARGS" in 1) case "$OPTION" in -h|--help) Usage EXIT_CODE=0 ;; -v|--version) Version EXIT_CODE=0 ;; esac ;; 2) # help options if [ "$OPTION" == "--help" ] || [ "$OPTION" == "-h" ]; then if [ "$ARG" == "config" ]; then ConfigHelp EXIT_CODE=0 else echo >&2 "$BASENAME: invalid help section -- $ARG" Usage EXIT_CODE=1 fi # if firma.conf exists and can be read elif [[ ! -r "$FIRMA_CONFIG_FILE" ]]; then LogMessage "FATAL: Cannot source \`$FIRMA_CONFIG_FILE': No such file or directory" EXIT_CODE=1 # then source and evaluate its parameters elif ! { SourceFirmaConfig && \ CheckFirmaConfigFile && \ CheckPermission "$FIRMA_CONFIG_FILE" }; then EXIT_CODE=1 else LIST_NAME="$ARG" LIST_PATH="$LISTS_DIR/$LIST_NAME" LIST_CONFIG_FILE="$LIST_PATH/$LIST_NAME.conf" case "$OPTION" in -c|--create-newlist) NewList EXIT_CODE=$? ;; # options that depend on the list configuration file -a|--admin-task|-e|--email-admin-task|-p|--process-message) # if list config file exists and can be read if [[ ! -r "$LIST_CONFIG_FILE" ]]; then LogMessage "FATAL: Cannot source \`$LIST_CONFIG_FILE': No such file or directory" EXIT_CODE=1 # then source and evaluate its parameters elif ! { SourceListConfig && \ DeclareGpgVars && \ CheckListPermissions "$LIST_CONFIG_FILE" && \ CheckListConfigFile }; then EXIT_CODE=1 else case "$OPTION" in -a|--admin-task) MODE="admin-interactive" # while a "quit" command isn't entered (returns 3), read STDIN while (( $EXIT_CODE != 3 )) && 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) MODE="list-message" ProcessMessage EXIT_CODE=$? ;; -e|--email-admin-task) MODE="admin-non-interactive" ProcessMessage EXIT_CODE=$? ;; esac fi ;; esac fi ;; esac fi # exit exit $EXIT_CODE