#!/usr/bin/env bash # MSP.sh: MSP-like mail server log parser in Bash # Nathan P. # 0.2b (Postfix) set -euo pipefail # Version and mail server variant # Right now MSP.sh supports Postfix and has a WIP version for Exim VERSION='0.2b (Postfix)' # Nice text formatting options TEXT_BOLD='\e[1m' TEXT_RED='\e[31m' TEXT_GREEN='\e[32m' TEXT_UNSET='\e[0m' # Path to the Postfix log file. We do rotation stuff further down. LOG_FILE='/var/log/maillog' # The RBLs we check against RBL_LIST=('b.barracudacentral.org' 'bl.spamcop.net' 'dnsbl.sorbs.net' 'spam.dnsbl.sorbs.net' 'ips.backscatterer.org' 'zen.spamhaus.org') # Help message # Email Steve to discuss current rates and senior discounts show_help() { cat << EOF $(echo -e "${TEXT_BOLD}")MSP.sh:$(echo -e "${TEXT_UNSET}") MSP-like mail server log parser in Bash USAGE: MSP.sh [OPTION] --auth Show mail server stats --rotated Use rotated log files --rbl Check IPs against common RBLs --help -h Show this message --version -v Show version information EOF } # --auth/mail server stats run auth_check() { # There's no reason to run all this if there's no log file... if ! [[ -e "${LOG_FILE}" ]]; then echo "No Postfix log found. Check for ${LOG_FILE} or update LOG_FILE in MSP.sh." return fi # If --rotated is passed, we fetch the rotated logs as well using find. Not # sure if the basename stuff below is smart or really dumb (maybe both?). # shellcheck disable=SC2312 if [[ "${use_rotated}" = true ]]; then mapfile -t LOG_FILE< <(find /var/log -maxdepth 1 -type f \( -name "$(basename "${LOG_FILE}")" -o -name "$(basename "${LOG_FILE}")-*" \) | sort) echo -e "${TEXT_BOLD}Heads up:${TEXT_UNSET} Using rotated log files. This could take a bit longer." echo fi echo 'Getting cool Postfix facts...' echo # Dead simple queue size check. Might expand this in the future to alert if # the queue size is too high. # shellcheck disable=SC2312 queue_size="$(postqueue -j 2>/dev/null | wc -l)" echo -e "📨 ${TEXT_BOLD}Queue Size:${TEXT_UNSET} ${queue_size}" echo "There's nothing else to show here. Have a llama: 🦙" echo # These are senders that have logged in to actual email accounts echo -e "🔑 ${TEXT_BOLD}Authenticated Senders${TEXT_UNSET}" # First fetch our list of senders into an array # shellcheck disable=SC2312 if grep -q 'sasl_username=' "${LOG_FILE[@]}"; then auth_senders="$(grep 'sasl_username=' "${LOG_FILE[@]}" \ | awk -F 'sasl_username=' '{print $2}' | awk '{print $1}')" else auth_senders= fi # Now sort them into the top 10 results if [[ -n "${auth_senders[*]}" ]]; then # shellcheck disable=SC2312 for sender in ${auth_senders}; do echo "${sender}" done | sort | uniq -c | sort -rn | head -n 10 fi # Or report if no results were found if [[ -z "${auth_senders[*]}" ]]; then echo 'No authenticated senders found.' fi echo # Directories where unauthenticated mail was sent... IF POSTFIX SUPPORTED # THIS. Somebody running a shared mail server should definitely be using # Exim. Postfix is much too simple, which is great, except for stuff like # detailed logging, which is needed for that use case. This is also where # I admit that I do not like Postfix. #echo -e "📂 ${TEXT_BOLD}Directories${TEXT_UNSET}" #echo # System users that have sent mail (like with PHP's mail function) echo -e "🧔 ${TEXT_BOLD}User Senders${TEXT_UNSET}" # First fetch our list of senders into an array # shellcheck disable=SC2312 if grep -q 'uid=' "${LOG_FILE[@]}"; then user_senders="$(grep 'uid=' "${LOG_FILE[@]}" \ | awk -F 'uid=' '{print $2}' | awk '{print $1}')" else user_senders= fi # Now sort them into the top 10 results if [[ -n "${user_senders[*]}" ]]; then # shellcheck disable=SC2312 for user in ${user_senders}; do getent passwd "${user}" | awk -F ':' '{print $1}' done | sort | uniq -c | sort -rn | head -n 10 fi # Or report if no results were found if [[ -z "${user_senders[*]}" ]]; then echo 'No user senders found.' fi echo # Postfix supports logging subjects as well, but it's not enabled in the # default configuration. I've written instructions at # https://github.com/tchbnl/MSP.sh/LOGGING.md. # Yes I named this function so it lines up with the others. I'm like that. echo -e "💌 ${TEXT_BOLD}The Usual Subjects™${TEXT_UNSET}" # First fetch our list of subjects into an array # shellcheck disable=SC2312 if grep -q 'Subject:' "${LOG_FILE[@]}"; then subjects="$(grep 'Subject:' "${LOG_FILE[@]}" \ | awk -F 'header Subject: ' '{print $2}' \ | awk -F ' from localhost\\[127.0.0.1\\]' '{print $1}')" else subjects= fi # Now sort them into the top 10 results if [[ -n "${subjects[*]}" ]]; then # shellcheck disable=SC2312 for subject in "${subjects[@]}"; do echo "${subject}" done | sort | uniq -c | sort -rn | head -n 10 fi # Or report if no results were found if [[ -z "${subjects[*]}" ]]; then echo 'No subjects found (or subject logging is disabled in Postfix).' fi } # --rbl/RBL check # TODO: Add support for IPv6 addresses. Oh God. rbl_check() { if ! command -v dig >/dev/null; then echo 'dig not found. MSP.sh requires dig to check IPs against RBLs.' exit fi # Get our list of public IPs # shellcheck disable=SC2312 server_ips="$(hostname -I | xargs -n 1 \ | grep -Ev '^10.0.0|^127.0.0.1|^172.16.0|^192.168.0|^169.254.0|::')" # Not sure when this'd happen, but just in case... if [[ -z "${server_ips}" ]]; then echo 'No IP addresses found. MSP.sh checks public IPv4 addresses.' return fi echo 'Running RBL checks...' # And loop through each one for ip in ${server_ips}; do # We need to reverse the IP order for checks to "work" # This is a quick and dirty solution and will need to be replaced when # I add IPv6 support. I'm not looking forward to that. reversed_ip="$(echo "${ip}" | awk -F '.' '{print $4 "." $3 "." $2 "." $1}')" echo echo -e "${TEXT_BOLD}${ip}${TEXT_UNSET}" # Now we loop through each RBL inside the IP loop for rbl in "${RBL_LIST[@]}"; do # These two RBLs are too short for only one tab :( if [[ "${rbl}" = 'bl.spamcop.net' || "${rbl}" = 'dnsbl.sorbs.net' ]]; then echo -ne "\t${rbl}\t\t" else echo -ne "\t${rbl}\t" fi # We dig our reversed IP against each RBL. This might not work for # all RBLs, but I haven't been able to fully test it. rbl_result="$(dig "${reversed_ip}"."${rbl}" +short)" # And now we return the block result depending on what the response # from dig was (none = good, something = bad) if [[ -n "${rbl_result}" ]]; then echo -e "${TEXT_BOLD}${TEXT_RED}LISTED${TEXT_UNSET}" else echo -e "${TEXT_BOLD}${TEXT_GREEN}GOOD${TEXT_UNSET}" fi done done } # Command options while [[ "${#}" -gt 0 ]]; do case "${1}" in --auth) shift 1 # New! Support for rotated log files. If --rotated is passed with # --auth, we round up the rotated logs as well into LOG_FILE... in # the function above. No nutso stuff in the while loop. if [[ "${#}" -gt 0 && "${1}" = '--rotated' ]]; then use_rotated=true else use_rotated= fi auth_check exit ;; # MSP requires --all to check all RBLs. We don't, but I figured I # should/might as well add this. --rbl | '--rbl --all') rbl_check exit ;; --help | -h) show_help exit ;; --version | -v) echo -e "${TEXT_BOLD}MSP.sh${TEXT_UNSET} ${VERSION}" exit ;; -*) echo -e "Not sure what ${1} is supposed to mean..." echo show_help exit ;; *) break ;; esac done # If no options are passed, we show the help message. I might renege on this # and default to running auth_check. show_help