#!/bin/bash


help() {
  cat << EOH

NAME
    $1 - check mail client connections via (reverse) DNS lookups

SYNOPSIS
    $1 [-s SERVER] [-d DELIMITER] [POSTFIX-LOG]
    $1 -h

DESCRIPTION
    This script reads a Postfix log from a log file or stdin and filters all
    lines with the pattern '/: connect from /', e.g.

      ... postfix/smtpd[...]: connect from helium.wu6ch.de[82.165.53.150]

    It reads the host name (now called 'in-name'), which might also be
    'unknown', and the IP address (now called 'in-addr') and adds a trailing
    line number (which is the number of the line in the original log file), e.g.

      420  helium.wu6ch.de  [82.165.53.150]

    The script performs a reverse lookup of 'in-addr' to get the related host
    name (called 'name') and a forward lookup with 'name' to get a list of the
    IP addresses ('addr-list'), related to 'name'.

    The reverse lookup might fail ('name' = 'NXDOMAIN'). The forward lookup
    might fail as well ('addr-list' = [] - empty list).

    The script outputs the header

      line  ind  in-name  in-addr  name  addr-list

    and the results of all queries on stdout as in the following example:

      420  ++  helium.wu6ch.de  [82.165.53.150]  helium.wu6ch.de  [82.165.53.150]
      \_/  ||  \_____________/   \___________/   \_____________/  \_____________/
       |   ||      in-name          in-addr           name           addr-list
       |   ||                                                        (here with
       |   ||                                                        just one
       |   |\-- forward-indicator ('+' or '~' or '-')                element)
       |   \--- reverse-indicator ('+' or '~' or '-')
       |
       \--------line number in the original log file

    reverse-indicator
      +  The reverse lookup was successful, and 'name' is equal to 'in-name'.
      ~  The reverse lookup was successful, but 'name' and 'in-name' differ.
      -  The reverse lookup failed ('name' is 'NXDOMAIN').

    forward-indicator
      +  The forward lookup was successful, and 'in-addr' is an element of 'addr-list'.
      ~  The forward lookup was successful, but 'addr-list' does not contain 'in-addr'.
      -  The forward lookup failed ('addr-list' is empty).

OPTIONS
    -s SERVER
        Name or IP address of the name server to query. If '-s' is not set,
        '/etc/resolv.conf' is consulted.

    -d DELIMITER
        Delimiter of elements in a line. The default is the TAB character. The
        TAB character allows an easy import into LibreOffice Calc for further
        filtering.

    -h
        This "manual page".

    POSTFIX-LOG
        Path to POSTFIX-LOG, typically '/var/log/mail.log'. If POSTFIX-LOG is
        not given, the data is expected via stdin.

EXAMPLE
    $1 /var/log/mail.log > mail_client_connections.tsv

AUTHOR
    Wolfgang <w6g@wu6ch.de>

LICENSE
    MIT (https://wu6ch.de/bash/functional_bash/LICENSE)

SEE ALSO
    Functional Bash (https://wu6ch.de/bash/functional_bash/)
EOH
}


set_env() {
  source "/usr/local/lib/functional_bash.sh"
  declare -gr host="host"

  # shellcheck disable=SC2034
  local -A options=()
  local -a remaining_args=()
  local    server

  get_options "s:d:" options remaining_args help "$@" && {
    server="$(get_arg options "s"     )"
    delim="$( get_arg options "d" "\t")"

    server_opt=()
    is_set "$server" && server_opt=( "${server}" )

    set -- "${remaining_args[@]}"
    log_file="${1-"-"}"

    declare -gr delim server_opt log_file
  }
}


put_header() {
  # -> "line $delim ind $delim in-name $delim in-addr $delim name $delim addr-list"
  echo -e "line${delim}ind${delim}in-name${delim}in-addr${delim}name${delim}addr-list"
}


filter_conn() {
  #    "420 ... postfix/smtpd[...]: connect from helium.wu6ch.de[82.165.53.150]"
  # -> "420/helium.wu6ch.de/82.165.53.150"

  sed -n 's/^ *\([0-9]\+\).*: \+connect \+from \+\(.*\)\[\(.*\)\].*$/\1\/\2\/\3/p'
}


query_dns() {
  # $1: RR type
  # $2: to be queried

  "$host" -t "$1" "$2" "${server_opt[@]}"
}


put_query_result() {
  # $1: query response
  # $2: resource record

  if [ -n "$2" ]
  then
    echo "$2"
  else
    sed -n 's/^.*(\(.*\))$/\1/p' <<< "$1"
  fi
}


get_ptr_rr() {
  # $1: $in_addr

  local response ; response="$(query_dns "PTR" "$1")"
  local rr ; rr="$(sed -n 's/^.*domain name pointer \(.*\)\.$/\1/p' <<< "$response")"
  put_query_result "$response" "$rr"
}


get_addess_rrs() {
  # $1: $in_addr
  # $2: $name

  [ "$2" == "NXDOMAIN" ] || {
    local rr_type ; grep -q ":" <<< "$1" ; rr_type="$(put_cond "A" "AAAA" $?)"
    local response ; response="$(query_dns "$rr_type" "$2")"
    local rr ; rr="$(sed -n 's/^.*has \(IPv6 \)\?address \(.*\)$/\2/p' <<< "$response")"
    put_query_result "$response" "$rr"
  }
}


check_reverse() {
  # $1: $in_name
  # $2: $name

  if [ "$2" == "NXDOMAIN" ]
  then
    echo "-"
  elif [ "$2" == "$1" ]
  then
    echo "+"
  else
    echo "~"
  fi
}


check_forward() {
  # $1: $in_addr
  # $2: $addr_list

  if [ -z "$2" ]
  then
    echo "-"
  elif grep -q -F "$1" <<< "$2"
  then
    echo "+"
  else
    echo "~"
  fi
}


put_delimited_list() {
  # shellcheck disable=SC2317
  _put_element() {
    [ -n "$1" ] && echo -e -n "${1}${delim}"
    echo "[${2}]"
  }

  lfoldl "" _put_element
}


put_conn_data() {
  # $1: $lnum
  # $2: $in_name
  # $3: $in_addr
  # $4: $name
  # $5: $addr_list

  local r ; r="$(check_reverse "$2" "$4")"
  local f ; f="$(check_forward "$3" "$5")"

  printf \
    "%5d${delim}%s%s${delim}%s${delim}[%s]${delim}%s${delim}" \
    "$1" "$r" "$f" "$2" "$3" "$4"

  put_delimited_list <<< "$5"
}


hdl_conn_data() {
  local lnum in_name in_addr
  local name addr_list

  # 420/helium.wu6ch.de/82.165.53.150
  #    ^               ^
  #    | <-            |               -> 1) 420
  #                    | <-            -> 2) 420/helium.wu6ch.de
  #                                             ^
  #                                          -> |                 -> 3) helium.wu6ch.de
  #                    ^
  #                 -> |                -> 4) 82.165.53.150

  lnum=${1%%/*}           # 1) delete longest match of pattern from the end
  in_name=${1%/*}         # 2) delete shortest match of pattern from the end
  in_name=${in_name#*/}   # 3) delete shortest match of pattern from the beginning
  in_addr=${1##*/}        # 4) delete longest match of pattern from the beginning

  name="$(get_ptr_rr "$in_addr")"
  addr_list="$(get_addess_rrs "$in_addr" "$name")"

  put_conn_data "$lnum" "$in_name" "$in_addr" "$name" "$addr_list"
}


set_env "$@" \
  && is_installed "$host" \
  && is_readable "$log_file" \
  && put_header \
  && cat -n "$log_file" | filter_conn | lmap hdl_conn_data


# EOF - vim:cc=91
