#!/bin/bash

set -e

##############################  CONFIGURATION   ##############################

TEMP=`getopt -o vf:c:p:j:t: \
      --long verbose,emails-file:,cidrs-file:,processes:,workers:,nomx:,o1:,timeout: \
      -- "$@"`
if [ $? != 0 ]; then
    echo "Error in parsing options." >&2
    exit 1
fi
eval set -- "$TEMP"

# defaults
EMAILS_FILE="./email.txt"
MASKS_FILE="./cidr.txt"
WORKER_NUM=100
TIMEOUT=1

# parse options
while true; do
    case "$1" in
	-v|--verbose) VERBOSE=y; shift;;
	-f|--emails-file) EMAILS_FILE="$2"; shift 2;;
	-c|--cidrs-file) MASKS_FILE="$2"; shift 2;;
	-p|-j|--processes|--workers) WORKER_NUM="$2"; shift 2;;
	--nomx|--o1) NOMX_FILE="$2"; shift 2;;
	-t|--timeout) TIMEOUT="$2"; shift 2;;
	--) shift; break;;
	*) echo "Unknown option: $1" >&2; exit 2;;
    esac
done

# post init
MATCH_FILE="$EMAILS_FILE".match
UNMATCH_FILE="$EMAILS_FILE".unmatch
NOMX_FILE=${NOMX_FILE:-"$EMAILS_FILE".nomx}
DIG="`which dig` +short +time=$TIMEOUT"

# tradition
if [ -n "$VERBOSE" ]; then
    cat <<EOF
---------- Configuration ----------
EMAILS_FILE=$EMAILS_FILE
MASKS_FILE=$MASKS_FILE
WORKER_NUM=$WORKER_NUM
TIMEOUT=$TIMEOUT
MATCH_FILE=$MATCH_FILE
UNMATCH_FILE=$UNMATCH_FILE
NOMX_FILE=$NOMX_FILE
DIG=$DIG
-----------------------------------
EOF
fi

############################## IP ADDRS / MASKS ##############################

function addr2int {
    local a b c d addr="$1"
    IFS="." read a b c d <<<"$addr"
    echo "$((a*256**3 + b*256**2 + c*256 + d))"
}
# % addr2int 0.0.1.0
# 256

function int2addr () {
    local addr sep int="$1"
    for e in {3..0}
    do
        ((octet = int / (256 ** e) ))
        ((int -= octet * 256 ** e))
	addr+=$sep$octet
        sep=.
    done
    echo "$addr"
}
# % int2addr $(addr2int 1.2.3.4)
# 1.2.3.4

function int2addr_bin () {
    local addr sep int="$1"
    for e in {3..0}
    do
        ((octet = int / (256 ** e) ))
        ((int -= octet * 256 ** e))
	addr+=$sep$(printf "%08d" $(bc <<< "ibase=10; obase=2; $octet"))
        sep=.
    done
    echo "$addr"
}
# % int2addr_bin $(addr2int 1.2.3.4) 
# 00000001.00000010.00000011.00000100

function make_mask () {
    local mask=2#`printf "%$1s" | tr " " "1"`
    echo $((mask<<(32-$1)))
}

# % int2addr_bin $(make_mask 11)
# 11111111.11100000.00000000.00000000

FULL_MASK=`make_mask 32`
function invert_mask () {
    local mask="$1"
    echo $((FULL_MASK-mask))
}
# % int2addr_bin $(invert_mask $(make_mask 31)) 
# 00000000.00000000.00000000.00000001

# returns lower and upper bound integers for this mask
function parse_mask () {
    local addr num
    IFS="/" read addr num <<<"$1"
    local mask=`make_mask $num`
    local int=`addr2int $addr`
    local lower=$((int & mask))
    local upper=$((int | `invert_mask $mask`))
    echo $lower $upper
}
# % parse_mask 0.0.1.0/24   
# 256 511

function check_ip_with_range {
    local addr="$1" lower="$2" upper="$3"
    local int=`addr2int $addr`
    echo -n ""
    [ "$int" -ge "$lower" ] && [ "$int" -le "$upper" ]
    return $?
}
# % read lower upper < <(read_mask 0.0.1.0/24)
# % echo $lower $upper
# 256 511
# % check_ip_with_range 0.0.1.0 $lower $upper && echo True || echo False 
# True
# % check_ip_with_range 0.0.2.1 $lower $upper && echo True || echo False
# False

##############################    DNS SUPPORT   ##############################

# ip address of the *main* mx record
function mxdn2ip {
    local domain="$1" prio=1000
    local mxprio mxdn addrs ip dn
    while read mxprio mxdn; do
	if [ "$mxprio" = ";;" ]; then # dig output ";; connection timed out; no servers could be reached"
	    return 0
	elif [ -n "$mxprio" ] && [ -z "$mxdn" ]; then # some MX records don't have priority (see Note below)
	    prio=0
	    dn="$mxprio"
	elif [ -n "$mxprio" ] && [ "$mxprio" -lt "$prio" ]; then
	    prio="$mxprio"
	    dn="$mxdn"
	fi
    done < <($DIG -t MX "$domain" || return 0)
    [ -z "$dn" ] && return 0
    ip=`$DIG $dn`
    echo "$ip"
}
# % dig +short -t MX gmail.com 
# ...
# 5 gmail-smtp-in.l.google.com.
# ...
# % dig +short gmail-smtp-in.l.google.com.
# 64.233.162.27
# % mxdn2ip gmail.com
# 64.233.162.27

# Note: found that sometimes MX records don't have priority:
# $ dig +short +time=10 -t MX jzaa.cn
# j.dcoin.co.
# 5 io.bouncemx.com.

##############################      LOGIC       ##############################

# 1) generate file with ip ranges
# originally I wanted to use environment variable, but it seems the
# environment becomes to big with such a lot of ranges

export RANGES_FILE=$(mktemp -t ranges.XXXXXXXX)
exec 3>$RANGES_FILE
function clean_tmp {
    rm "$RANGES_FILE"
}
trap clean_tmp EXIT # remove file before exit
#echo "RANGES: $RANGES_FILE"

echo -n "Parsing CIDRs... "
while read mask; do
    parse_mask $mask >&3 # redirect to RANGES
#done < <(head -5 $MASKS_FILE)
done <$MASKS_FILE
exec 3>&-

echo ok

# 2) open appropriate file descriptors for output files
# they are inherited when process forks so it is useful for not doing more open/close syscalls

exec 101>"$MATCH_FILE"
exec 102>"$UNMATCH_FILE"
exec 103>"$NOMX_FILE"

# 3) starting checker daemon
echo -n "Starting checker daemon..."
./checker "$RANGES_FILE" &>/dev/null &
echo ok

echo "Processing emails now"

function check_ip {
    local ip=$1
    while read lower upper; do
	if [ -n "$lower" ]; then
	    check_ip_with_range "$ip" "$lower" "$upper" && return 0
	fi
    done <"$RANGES_FILE"
    return 1
}

function check_ip_with_daemon {
    local ip="$1"
    local num=`addr2int "$ip"`
    case `addr2int "$ip" | socat - UNIX-CLIENT:/tmp/checker.socket` in
	true) return 0;;
	false) return 1;;
	*) return 2;;
    esac
}

function process_email {
    local domain email="$1"
    if [ -z "$email" ]; then echo "fuck"; exit 7; fi
    IFS="@" read _ domain <<<"$email"
    local ips=`mxdn2ip "$domain"`
    local match=
    if [ -z "$ips" ]; then
	echo "$email" >&103
    else
	while read ip; do
	    if [ -n "$ip" ]; then
		#if check_ip "$ip"; then
		if check_ip_with_daemon "$ip"; then
		    match=y
		fi
	    fi
	done <<<"$ips"
	if [ -n "$match" ]; then
	    echo "$email" >&101
	else
	    echo "$email" >&102
	fi
    fi
}

# this stuff must be in environment to use in the child bash process
#export -f check_ip
#export -f check_ip_with_range
export -f check_ip_with_daemon
export -f process_email
export -f mxdn2ip
export -f addr2int
export DIG
export RANGES_FILE

<"$EMAILS_FILE" xargs -n1 -P "$WORKER_NUM" bash -c 'process_email $1 2>/dev/null' --
#<"$EMAILS_FILE" xargs -n1 -P "$WORKER_NUM" bash -c 'process_email $1 ' --
exit 0

while read email; do
    time process_email "$email"
    echo
    sleep 1
done <$EMAILS_FILE


exit 0

while read lower upper; do
    if [ -n "$upper" ]; then
	echo lower: $lower
	echo upper: $upper
	echo
    fi
done <"$RANGES_FILE"