{-# LANGUAGE DoAndIfThenElse #-}
{-# LANGUAGE RecordWildCards #-}

module Main (main)
where

import Control.Concurrent.ParallelIO.Global (
   parallelInterleaved,
   stopGlobalPool )
import qualified Data.ByteString.Char8 as BS (
  hGetContents,
  hPutStrLn,
  lines,
  null,
  pack )
import Network.DNS (
  Domain,
  Resolver,
  ResolvConf( resolvTimeout ),
  defaultResolvConf,
  makeResolvSeed,
  withResolver )
import Network.DNS.Lookup ( lookupA, lookupMX )
import System.IO (
  hFlush,
  stdin,
  stdout )


import CommandLine (
  Args( Args, accept_a, rfc5322 ),
  get_args )
import EmailAddress(
  Address,
  parts,
  validate_syntax )


-- | Resolver parameters. We increase the default timeout from 3 to 10
--   seconds.
resolv_conf ::  ResolvConf
resolv_conf = defaultResolvConf { resolvTimeout = 10 * 1000 * 1000 }

-- | A list of common domains, there's no need to waste MX lookups on
--   these. This is a very limited list; I don't want to be in the
--   business of monitoring a million domains for MX record updates.
common_domains :: [Domain]
common_domains = map BS.pack [ "aol.com",
                               "comcast.net",
                               "cox.net",
                               "gmail.com",
                               "gmx.de",
                               "googlemail.com",
                               "hotmail.com",
                               "icloud.com",
                               "live.com",
                               "me.com",
                               "msn.com",
                               "outlook.com",
                               "proton.me",
                               "protonmail.ch",
                               "protonmail.com",
                               "yahoo.com",
                               "verizon.net" ]


-- | Check whether the given domain has a valid MX record.
--
--   NULLMX (RFC7505) records consisting of a single period must not
--   be accepted. Moreover, the existence of a NULLMX must be reported
--   back to the caller because the whole point of a NULLMX is that
--   its existence should preempt an @A@ record check. We abuse the
--   return type for this, and return @Nothing@ in the event of a
--   NULLMX. Otherwise we return @Just True@ or @Just False@ to
--   indicate the existence (or not) of MX records.
--
--   RFC7505 states that a domain MUST NOT have any other MX records
--   if it has a NULLMX record. We enforce this. If you have a NULLMX
--   record and some other MX record, we consider the set invalid.
--
validate_mx :: Resolver -> Domain -> IO (Maybe Bool)
validate_mx resolver domain
  | domain `elem` common_domains = return $ Just True
  | otherwise = do
      result <- lookupMX resolver domain
      case result of
        Left _ ->
            return $ Just False
        Right mxs ->
            case mxs of
              [] -> return $ Just False
              _  -> if any (is_null) mxs
                   then return Nothing
                   else return $ Just True
  where
    nullmx :: Domain
    nullmx = BS.pack "."

    is_null :: (Domain,Int) -> Bool
    is_null (mx,prio) = mx == nullmx && prio == 0

-- | Check whether the given domain has a valid A record.
validate_a :: Resolver -> Domain -> IO Bool
validate_a resolver domain
  | domain `elem` common_domains = return True
  | otherwise = do
      result <- lookupA resolver domain
      case result of
        Right (_:_) -> return True
        _           -> return False


-- | Validate an email address by doing some simple syntax checks and
--   (if those fail) an MX lookup. We don't count an @A@ record as a mail
--   exchanger unless @accept_a@ is True. And even then, the existence
--   of a NULLMX record will preclude the acceptance of an @A@ record.
--   The domain @example.com@ is a great test case for this behavior.
validate :: Resolver -> Bool -> Bool -> Address -> IO (Address, Bool)
validate resolver accept_a rfc5322 address = do
  let valid_syntax = validate_syntax rfc5322 address
  if valid_syntax then do
    let (_,domain) = parts address
    mx_result <- validate_mx resolver domain
    case mx_result of
      Nothing  ->
          -- NULLMX, don't fall back to 'A' records under any
          -- circumstances.
          return (address, False)
      Just mxr ->
          if mxr
          then return (address, True)
          else
            if accept_a
            then do
              a_result <- validate_a resolver domain
              return (address, a_result)
            else
              return (address, False)
          else
            return (address, False)


main :: IO ()
main = do
  Args{..} <- get_args

  -- Split stdin into lines, which should result in a list of addresses.
  input <- BS.hGetContents stdin
  let addresses = BS.lines input

  -- And remove the empty ones.
  let nonempty_addresses = filter (not . BS.null) addresses

  rs <- makeResolvSeed resolv_conf
  let validate' addr = withResolver rs $ \resolver ->
                         validate resolver accept_a rfc5322 addr

  -- Construct a list of [IO (Address, Bool)]. The withResolver calls
  -- are the ones that should be run in parallel.
  let actions = map validate' nonempty_addresses

  -- Run the lookup actions in parallel.
  results <- parallelInterleaved actions

  -- Filter the bad ones.
  let valid_results = filter snd results

  -- Output the results.
  let valid_addresses = map fst valid_results
  mapM_ (BS.hPutStrLn stdout) valid_addresses

  stopGlobalPool
  hFlush stdout
