module Main
where
-import Control.Concurrent.ParallelIO.Global (parallel, stopGlobalPool)
-import Control.Monad (unless)
-import qualified Data.ByteString as BS
-import qualified Data.ByteString.UTF8 as BSU
+import Control.Concurrent.ParallelIO.Global (
+ parallelInterleaved,
+ stopGlobalPool )
+import Control.Monad ( unless )
+import qualified Data.ByteString.Char8 as BS (
+ hGetContents,
+ hPutStrLn,
+ lines,
+ null,
+ pack,
+ readFile )
import Network.DNS (
Domain,
Resolver,
ResolvConf(..),
defaultResolvConf,
makeResolvSeed,
- withResolver)
-import Network.DNS.Lookup (lookupMX)
-import System.Directory (doesFileExist)
-import System.Exit (exitWith, ExitCode(..))
+ withResolver )
+import Network.DNS.Lookup ( lookupA, lookupMX )
+import System.Directory ( doesFileExist )
+import System.Exit ( exitWith, ExitCode(..) )
import System.IO (
- Handle,
IOMode( WriteMode ),
hClose,
hFlush,
openFile,
stdin,
- stdout)
+ stdout )
-import CommandLine (Args(..), apply_args)
-import EmailAddress
-import ExitCodes (exit_input_file_doesnt_exist)
+import CommandLine ( Args(..), get_args )
+import EmailAddress(
+ Address,
+ parts,
+ validate_syntax )
+import ExitCodes ( exit_input_file_doesnt_exist )
--- | Resolver parameters. We increase the default timeout from 3 to 5
+-- | Resolver parameters. We increase the default timeout from 3 to 10
-- seconds.
resolv_conf :: ResolvConf
-resolv_conf = defaultResolvConf { resolvTimeout = 5 * 1000 * 1000 }
+resolv_conf = defaultResolvConf { resolvTimeout = 10 * 1000 * 1000 }
-- | A list of common domains, there's no need to waste MX lookups
-- on these.
common_domains :: [Domain]
-common_domains = map BSU.fromString [ "aol.com",
- "comcast.net",
- "gmail.com",
- "msn.com",
- "yahoo.com",
- "verizon.net" ]
+common_domains = map BS.pack [ "aol.com",
+ "comcast.net",
+ "gmail.com",
+ "msn.com",
+ "yahoo.com",
+ "verizon.net" ]
-- | Check whether the given domain has a valid MX record.
| otherwise = do
result <- lookupMX resolver domain
case result of
- Nothing -> return False
- _ -> return True
+ -- A list of one or more elements?
+ Right (_:_) -> return True
+ _ -> return False
--- | Validate the syntax of an email address by checking its length
--- and validating it against a simple regex.
-validate_syntax :: Address -> Bool
-validate_syntax address =
- (validate_length address) && (validate_regex address)
+-- | 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.
-validate :: Resolver -> Address -> IO (Address, Bool)
-validate resolver address = do
- let valid_syntax = validate_syntax address
+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
- return (address, mx_result)
+ if mx_result
+ 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)
--- | Append a ByteString to a file Handle, followed by a newline.
-append_handle_with_newline :: Handle -> BS.ByteString -> IO ()
-append_handle_with_newline h bs = do
- BS.hPutStr h bs
- BS.hPutStr h newline
- where
- newline = BSU.fromString "\n"
-
main :: IO ()
main = do
- Args{..} <- apply_args
+ Args{..} <- get_args
-- Get the input from either stdin, or the file given on the command
-- line.
Just path -> openFile path WriteMode
-- Split the input into lines.
- let addresses = BSU.lines input
+ let addresses = BS.lines input
-- And remove the empty ones.
let nonempty_addresses = filter (not . BS.null) addresses
rs <- makeResolvSeed resolv_conf
- withResolver rs $ \resolver -> do
- -- Construst a list of [IO (Address, Bool)]
- let actions = map (validate resolver) nonempty_addresses
- -- And compute them in parallel.
- results <- parallel actions
- stopGlobalPool
- -- Find the pairs with a True in the second position.
- let good_pairs = filter snd results
- -- And output the results.
- mapM_ ((append_handle_with_newline output_handle) . fst) good_pairs
+ 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 output_handle) valid_addresses
+
+ stopGlobalPool
-- Clean up. It's safe to try to close stdout.
hFlush output_handle