1 {-# LANGUAGE DoAndIfThenElse #-}
2 {-# LANGUAGE RecordWildCards #-}
7 import Control.Concurrent.ParallelIO.Global (parallel, stopGlobalPool)
8 import Control.Monad (unless)
9 import qualified Data.ByteString as BS
10 import qualified Data.ByteString.UTF8 as BSU
18 import Network.DNS.Lookup (lookupMX)
19 import System.Directory (doesFileExist)
20 import System.Exit (exitWith, ExitCode(..))
31 import CommandLine (Args(..), apply_args)
33 import ExitCodes (exit_input_file_doesnt_exist)
36 -- | Resolver parameters. We increase the default timeout from 3 to 5
38 resolv_conf :: ResolvConf
39 resolv_conf = defaultResolvConf { resolvTimeout = 5 * 1000 * 1000 }
41 -- | A list of common domains, there's no need to waste MX lookups
43 common_domains :: [Domain]
44 common_domains = map BSU.fromString [ "aol.com",
52 -- | Check whether the given domain has a valid MX record.
53 validate_mx :: Resolver -> Domain -> IO Bool
54 validate_mx resolver domain
55 | domain `elem` common_domains = return True
57 result <- lookupMX resolver domain
59 Nothing -> return False
63 -- | Validate the syntax of an email address by checking its length
64 -- and validating it against a simple regex.
65 validate_syntax :: Address -> Bool
66 validate_syntax address =
67 (validate_length address) && (validate_regex address)
70 -- | Validate an email address by doing some simple syntax checks and
71 -- (if those fail) an MX lookup. We don't count an A record as a mail
73 validate :: Resolver -> Address -> IO (Address, Bool)
74 validate resolver address = do
75 let valid_syntax = validate_syntax address
76 if valid_syntax then do
77 let (_,domain) = parts address
78 mx_result <- validate_mx resolver domain
79 return (address, mx_result)
81 return (address, False)
84 -- | Append a ByteString to a file Handle, followed by a newline.
85 append_handle_with_newline :: Handle -> BS.ByteString -> IO ()
86 append_handle_with_newline h bs = do
90 newline = BSU.fromString "\n"
95 Args{..} <- apply_args
97 -- Get the input from either stdin, or the file given on the command
99 input <- case input_file of
100 Nothing -> BS.hGetContents stdin
102 is_file <- doesFileExist path
104 exitWith (ExitFailure exit_input_file_doesnt_exist)
107 -- Do the same for the output handle and stdout.
108 output_handle <- case output_file of
109 Nothing -> return stdout
110 Just path -> openFile path WriteMode
112 -- Split the input into lines.
113 let addresses = BSU.lines input
115 -- And remove the empty ones.
116 let nonempty_addresses = filter (not . BS.null) addresses
118 rs <- makeResolvSeed resolv_conf
119 withResolver rs $ \resolver -> do
120 -- Construst a list of [IO (Address, Bool)]
121 let actions = map (validate resolver) nonempty_addresses
122 -- And compute them in parallel.
123 results <- parallel actions
125 -- Find the pairs with a True in the second position.
126 let good_pairs = filter snd results
127 -- And output the results.
128 mapM_ ((append_handle_with_newline output_handle) . fst) good_pairs
130 -- Clean up. It's safe to try to close stdout.