#!/bin/sh # # /home/anoncvs/report_spam/report_spam.sh,v 1.73 2024/08/14 18:10:58 bigby Exp # # report_spam # (C) 2018, Joseph Robinson # # Forward spam messages to the domain that relayed them to us. # # # # model: # # Received: from public-exrelay-ca1-1.serverdata.net (public-exrelay-ca1-1.serverdata.net [64.78.22.125]) # Received: from access.bodyfitnewss.review ([200.63.45.92]) # Received: from 197.237.193.210.wananchi.com (197.237.193.210.wananchi.com [197.237.193.210] (may be forged)) BASE=`basename "$0"` DEFAULT_SENDMAIL_OPTS="-N success,delay,failure" SENDMAIL_OPTS=${SENDMAIL_OPTS:-$DEFAULT_SENDMAIL_OPTS} BOUNDARY=`date +%s%s` DEFAULT_RECEIVED_IGNORE_REGEX="68b329da9893e34099c7d8ad5cb9c940" RECEIVED_IGNORE_REGEX=${RECEIVED_IGNORE_REGEX:-$DEFAULT_RECEIVED_IGNORE_REGEX} DEFAULT_SIGNATURE_FILES="" SIGNATURE_FILES=${SIGNATURE_FILES:-$DEFAULT_SIGNATURE_FILES} DEFAULT_TRY_SPAMASSASSIN="YES" TRY_SPAMASSASSIN=${TRY_SPAMASSASSIN:-$DEFAULT_TRY_SPAMASSASSIN} #DEFAULT_MX_IGNORE_REGEX="68b329da9893e34099c7d8ad5cb9c940" #MX_IGNORE_REGEX=${MX_IGNORE_REGEX:-$DEFAULT_MX_IGNORE_REGEX} # REQUEST_RECEIPT={success|failure|never|delay}[,{success|failure|never|delay}...] # WANT_HEADERS_REGEX="abuse@rima-tde.net" # WANT_WHOLE_MESSAGE_REGEX="" # for recipients we ignore; if recipient matches this regex, # throw the complaint away. # RECIPIENT_IGNORE_REGEX="" # MANGLE_example_com="test@otherdomain.net" # mangle/translate on a single IP address: # MANGLE_1_2_3_4="abuse@example.com" # # IGNORE_DNS_REGEX is for hosts/networks you want to only use whois # for abuse contacts, not DNS. IGNORE_DNS_REGEX="" MID_DB_FILE="$HOME/.report_spam.mid.db" LOOK_IN_MIDB=TRUE ############### # SUBROUTINES # ############### debug() { if [ "$DEBUG" ] then echo "debug:: $*" >&2 fi } readconfig() { if [ -f /etc/report_spam.conf ] then . /etc/report_spam.conf fi if [ -f $HOME/.report_spam.conf ] then . $HOME/.report_spam.conf fi } ignore_recipient() { if [ "$RECIPIENT_IGNORE_REGEX" ] then echo "$1" | egrep -iq "$RECIPIENT_IGNORE_REGEX" return else return 1 fi } ignore_dns() { if [ "$IGNORE_DNS_REGEX" ] then echo "$1" | egrep -iq "$IGNORE_DNS_REGEX" return else return 1 fi } ignore_mx() { # debug "ignore_mx: starting" local DOMAIN STATUS DOMAIN=`echo $1 | sed 's/.*@//'` # debug "ignore_mx: doing lookup on DOMAIN=$DOMAIN" if [ "$MX_IGNORE_REGEX" ] then # debug "ignore_mx: executing dig +short $DOMAIN mx" dig +short $DOMAIN mx | egrep -iq "$MX_IGNORE_REGEX" STATUS=$? # debug "ignore_mx: STATUS for mx dig was $STATUS" return $STATUS else # debug "ignore_mx: MX_IGNORE_REGEX empty, returning 1" return 1 fi } want_whole_message() { if [ "$WANT_WHOLE_MESSAGE_REGEX" ] then echo $1 | egrep -q "$WANT_WHOLE_MESSAGE_REGEX" return else return 1 fi } print_whole_message() { echo "" echo "Here is the entire message:" echo "" echo "------------------------- BEGIN MESSAGE -------------------------" cat "$1" echo "------------------------- END MESSAGE -------------------------" echo "" } want_headers() { if [ "$WANT_HEADERS_REGEX" ] then echo $1 | egrep -q "$WANT_HEADERS_REGEX" return else return 1 fi } print_headers() { echo "" echo "Here are the headers of the message:" echo "" echo "------------------------- BEGIN HEADERS -------------------------" cat "$1" | sed '/^$/,$d' echo "------------------------- END HEADERS -------------------------" echo "" } check_spamassassin() { case $TRY_SPAMASSASSIN in [Yy]*|[Tt][Rr][Uu][Ee]) if `which spamassassin > /dev/null` then USE_SPAMASSASSIN=1 debug "USE_SPAMASSASSIN set to $USE_SPAMASSASSIN" fi ;; *) unset USE_SPAMASSASSIN debug "USE_SPAMASSASSIN unset" ;; esac } get_num_parts() { # return the number of parts in $1 # eg. test.example.com = 3 local VAL NUM VAL=$1 NUM=0 while echo $VAL | grep . > /dev/null do VAL=`echo $VAL | sed -e 's/[^\.]*//' -e 's/^\.//'` NUM=`expr $NUM + 1` done echo $NUM } return_parts() { # # return $2 parts of $1; chop off the leading # ( - $2) parts. local TOTAL_PARTS VAL CHOP VAL=$1 TOTAL_PARTS=`get_num_parts $1` CHOP=`expr $TOTAL_PARTS - $2` while [ $CHOP -gt 0 ] do VAL=`echo $VAL | sed 's/[^\.]*\.//'` CHOP=`expr $CHOP - 1` done echo $VAL } test_domain() { local TMPFILE TMPFILE=`mktemp /tmp/.report_spam.$$.test_domain.XXXXX` || exit # if dig returns with exit code zero, if dig +short $1 a > $TMPFILE 2>&1 then # and if the output contains no semi-colons, if ! egrep -q ';' $TMPFILE then # and if the output contains something if grep -q . $TMPFILE then # then it's a success/hit! rm -f $TMPFILE return 0 fi fi fi rm -f $TMPFILE # if dig returns with exit code zero, if dig +short $1 mx > $TMPFILE 2>&1 then # and if the output contains no semi-colons, if ! egrep -q ';' $TMPFILE then # and if the output contains something if grep -q . $TMPFILE then # then it's a success/hit! rm -f $TMPFILE return 0 fi fi fi rm -f $TMPFILE return 1 } get_domain() { local HOST PARTS MAX_PARTS TEST HOST=$1 PARTS=2 MAX_PARTS=`get_num_parts $HOST` while [ $PARTS -le $MAX_PARTS ] do TEST=`return_parts $HOST $PARTS` if test_domain $TEST then echo $TEST return fi PARTS=`expr $PARTS + 1` done return } cleanup() { if [ "$ISTMPFILE" ] then rm -f $FILE fi } print_signature() { local myfile for myfile in $SIGNATURE_FILES do if [ -f "$myfile" ] then cat "$myfile" break fi done } get_whois_abuse_email() { local IP WHOISTMPFILE GARBAGE IP=$1 if [ "x$IP" = "x" ] then return fi # attempt whois WHOISTMPFILE=`mktemp /tmp/.report_spam.$$.whoistmp.XXXXX` || exit whois $IP > $WHOISTMPFILE # another example: # % Abuse contact for '79.124.59.96 - 79.124.59.127' is 'abuse@herehost.com' # % Abuse contact for '156.54.213.0 - 156.54.213.255' is 'abuse-ripe@telecomitalia.it' GARBAGE=`grep "Abuse contact for" $WHOISTMPFILE | cut -f 4 -d\'` if [ "$GARBAGE" ] then echo $GARBAGE rm -f $WHOISTMPFILE return fi # example: # OrgAbuseEmail: abuse@uk2group.com GARBAGE=`awk ' tolower($1)~/abuse/ && $2~/@/ { print $2 ; exit} ' $WHOISTMPFILE` if [ "$GARBAGE" ] then echo $GARBAGE rm -f $WHOISTMPFILE return fi rm -f $WHOISTMPFILE return } mangle() { # # check $1 to see if a mangle # exists for it, and return # it if it does. local GARBAGE GARBAGE=`echo $1 | sed 's/[-.]/_/g'` eval GARBAGE=\$MANGLE_$GARBAGE echo $GARBAGE } msg_is_bounce() { test -e "$1" || return egrep -qi '^Content-Type:.*multipart/report;.*report-type=delivery-status;' "$1" return } get_message_id() { local FILE FILE="$1" MID=`egrep -i "^message-id: ..*" "$FILE" | sed "s/^[^:]*: *//" | head -1` if [ "$MID" ] then # debug "MID=$MID" return fi # couldn't find MID, maybe # it's on a continued line, # try that next MID=`egrep -i -A1 "^message-id:[ ]*" "$FILE" | head -2 | tail -1 | egrep "^[ ]" | sed "s/^[ ]*//"` if [ "$MID" ] then # debug "MID=$MID" return fi debug "get_message_id:: couldn't find MID" echo "$0: warning: Couldn't find message-id for file \"$FILE\"" >&2 return 1 } check_message_id() { local MID MID="$1" test -e $MID_DB_FILE || return test -f $MID_DB_FILE || return test -r $MID_DB_FILE || return grep -q -w "$MID" $MID_DB_FILE return } write_message_id_to_db() { local MID MID="$1" echo "$MID" >> $MID_DB_FILE || exit } handle_file() { local ORGFILE TO FILE HOST DOMAIN IP SUBJECT DATE WHOISTMPFILE GARBAGE ORGFILE=$1 TO="$USER_TO" unset ISTMPFILE test -r "$ORGFILE" || { echo "$BASE: CAN'T READ FILE \"$ORGFILE\", SKIPPING" >&2 return } if [ -f "$ORGFILE" ] then FILE="$ORGFILE" else FILE=`mktemp /tmp/.report_spam.$$.msg.XXXX` || exit ISTMPFILE=1 cp "$ORGFILE" "$FILE" || { echo "$BASE: CP $ORGFILE $FILE FAILED" rm -f "$FILE" exit 1 } fi # # test FILE to see if it is a bounce message if msg_is_bounce "$FILE" then echo "$0: FILE \"$FILE\" IS BOUNCE MESSAGE, SKIPPING" cleanup return fi # # get HOST relay for message HOST=`cat "$FILE" | \ dos2unix | \ sed '/^[ ]*$/,$'d | \ grep "^Received: from" | \ egrep -v "$RECEIVED_IGNORE_REGEX" | \ head -1 | \ sed -e 's/^Received: from [^(]*(\([^ ]*\) .*/\1/'` # DATE=`cat "$FILE" | grep "^Date: " | head -1 | sed 's/^Date: //'` # Received: from public-exrelay-ca1-1.serverdata.net (public-exrelay-ca1-1.serverdata.net [64.78.22.125]) # Received: from access.bodyfitnewss.review ([200.63.45.92]) # Received: from 197.237.193.210.wananchi.com (197.237.193.210.wananchi.com [197.237.193.210] (may be forged)) # # get IP of message relay IP=`cat "$FILE" | \ dos2unix | \ sed '/^[ ]*$/,$'d | \ grep "^Received: from" | \ egrep -v "$RECEIVED_IGNORE_REGEX" | \ head -1 | \ sed 's/^Received: from [^(]*([^[]*\[\([0-9]*\.[0-9]*\.[0-9]*\.[0-9]*\)].*)/\1/'` if [ ! "$TO" ] then # # if host or IP matches IGNORE_DNS_REGEX if ignore_dns $HOST then TO=`get_whois_abuse_email $IP` fi if ignore_dns $IP then TO=`get_whois_abuse_email $IP` fi fi if [ ! "$TO" ] then GARBAGE=`mangle $IP` if [ ! -z $GARBAGE ] then TO=$GARBAGE else if [ ! "$HOST" ] then echo "$BASE: COULDN'T GET HOST FOR FILE \"$FILE\"." >&2 cleanup return fi DOMAIN=`get_domain $HOST` if [ "$DOMAIN" ] then GARBAGE=`mangle $DOMAIN` if [ "$GARBAGE" ] then TO=$GARBAGE else TO=abuse@$DOMAIN fi else TO=`get_whois_abuse_email $IP` if [ "x$TO" = "x" ] then echo "$BASE: COULDN'T GET DOMAIN FOR HOST \"$HOST\", FILE \"$FILE\"." cleanup return fi fi fi fi if ignore_recipient "$TO" then echo "$BASE: SKIPPING RECIPIENT \"$TO\" IN FILE \"$FILE\": MATCHES RECIPIENT_IGNORE_REGEX." cleanup return fi if ignore_mx "$TO" then echo "$BASE: SKIPPING RECIPIENT \"$TO\" IN FILE \"$FILE\": MATCHES MX_IGNORE_REGEX." cleanup return fi # # check message ID against sent message database if [ "$LOOK_IN_MIDB" = "TRUE" ] then if get_message_id "$FILE" then if check_message_id $MID then echo "FOUND MID \"$MID\" FOR FILE \"$FILE\" IN DB, SKIPPING" return fi fi fi if [ "$DEBUG" ] then debug "would send mail from (${ENV_FROM:-$FROM}) \"$FROM\" to (${ENV_TO:-$TO}) \"$TO\" for FILE \"$FILE\"" else SUBJECT=`grep ^Subject: $FILE | head -1 | sed 's/Subject: //'` # # craft email ( echo "From: <$FROM>" echo "To: <$TO>" if [ "$CC" ] then echo "Cc: $CC" fi echo "Subject: FW: $SUBJECT" echo 'User-Agent: report_spam/1.73' echo "MIME-version: 1.0" echo "Content-type: multipart/report;" echo " BOUNDARY=$BOUNDARY" echo "" echo "--$BOUNDARY" echo "Content-type: text/plain" echo "" echo "This is an email abuse report for an email message received from IP" echo "$IP. For more information" echo "about this format please see http://www.mipassoc.org/arf/." echo "" if want_whole_message $TO then print_whole_message "$FILE" elif want_headers $TO then print_headers "$FILE" fi print_signature echo "--$BOUNDARY" echo "Content-type: message/feedback-report" echo "" echo "Feedback-Type: spam" echo 'User-Agent: report_spam/1.73' echo "Version: 1" echo "Source-IP: $IP" echo "--$BOUNDARY" echo "Content-type: message/rfc822" echo "" if [ "$USE_SPAMASSASSIN" ] then cat "$FILE" | spamassassin -d else cat "$FILE" fi echo "--${BOUNDARY}--" ) | sendmail $SENDMAIL_OPTS -f ${ENV_FROM:-$FROM} ${ENV_TO:-$TO}${CC:+,}${CC}${BCC:+,}${BCC} echo "$BASE: mail sent from ${ENV_FROM:-$FROM} to ${ENV_TO:-$TO}" write_message_id_to_db "$MID" fi cleanup if [ "$SLEEP" ] then if [ "$DEBUG" ] then debug "would sleep $SLEEP seconds" else echo "$BASE: sleeping $SLEEP seconds" sleep $SLEEP fi fi } usage() { echo "Usage:" echo "$BASE [-d] [-s ] [-r] [-b ] [-f ] [-F ] [-t ] [-T ] [-c ] [file ...]" echo "$BASE -h" echo " -d debugging mode: disable complaint sending, only output what would have been done." echo " -L do not Look up message ID in MID DB" echo " -r disable DSN success receipts (enabled by default)" echo " -R disable ALL DSN reports" echo " -s num sleep num seconds between sending each abuse report" echo " -S {yes|true|no|false} try to invoke SpamAssassin to remove SA markup from messages before sending" echo " -v give version information then quit" exit $1 } version() { echo 'report_spam 1.73' echo "Joseph Robinson " exit 0 } process_options() { while getopts "b:c:dhf:F:LrRt:T:s:S:v" OPT do case $OPT in b) BCC=$OPTARG debug "setting BCC=\"$BCC\"" ;; c) CC=$OPTARG debug "setting CC=\"$CC\"" ;; d) DEBUG=1 debug "debugging on" ;; f) FROM=$OPTARG debug FROM=$OPTARG ;; F) ENV_FROM=$OPTARG debug ENV_FROM=$OPTARG ;; h) usage 0 ;; L) LOOK_IN_MIDB="FALSE" debug setting LOOK_IN_MIDB to $LOOK_IN_MIDB ;; r) SENDMAIL_OPTS="-N delay,failure" debug sendmail success receipts disabled ;; R) SENDMAIL_OPTS="-N never" debug "Disabled all DSN reports" ;; s) SLEEP=$OPTARG debug "setting SLEEP=\"$SLEEP\"" ;; S) TRY_SPAMASSASSIN=$OPTARG debug "setting TRY_SPAMASSASSIN to \"$TRY_SPAMASSASSIN\"" ;; t) USER_TO=$OPTARG debug USER_TO=$OPTARG ;; T) ENV_TO=$OPTARG debug ENV_TO=$OPTARG ;; v) version ;; \?) usage 1 ;; esac done } first_sanity_check() { # # sanity check presence of dig which dig > /dev/null || { echo "ERROR: CAN'T FIND dig(1), EXITING." exit 1 } # # Sanity check FROM variable if [ "x$FROM" = "x" ] then echo "ERROR: CAN'T FIND FROM ADDRESS, EXITING" exit 1 fi } ######## # MAIN # ######## readconfig process_options "$@" first_sanity_check check_spamassassin while [ $OPTIND -gt 1 ] do shift OPTIND=`expr $OPTIND - 1` done if [ "$#" = 0 ] then if [ "$DEFAULT" ] then set -- $DEFAULT else set -- /dev/stdin fi fi # # everything else is arguments; # handle arguments while [ "$1" ] do if [ "$1" = "-" ] then handle_file /dev/stdin shift continue fi /bin/test -e "$1" || { echo "$BASE: ERROR: FILE \"$1\" DOESN'T EXIST, SKIPPING" >&2 shift continue } /bin/test -r "$1" || { echo "$BASE: ERROR: NO READ PERMISSION FOR FILE \"$1\", SKIPPING" >&2 shift continue } if [ -d "$1" ] then for file in "$1"/* do handle_file "$file" done else # # we'll assume that anything else # is a file, and treat it accordingly # so that things like pipes work # (eg. /dev/stdin) handle_file "$1" fi shift done