1 {-# LANGUAGE DoAndIfThenElse #-}
2 {-# LANGUAGE RecordWildCards #-}
7 import Control.Concurrent.ParallelIO.Global (
10 import qualified Data.ByteString.Char8 as BS (
19 ResolvConf( resolvTimeout ),
23 import Network.DNS.Lookup ( lookupA, lookupMX )
31 Args( Args, accept_a, rfc5322 ),
39 -- | Resolver parameters. We increase the default timeout from 3 to 10
41 resolv_conf :: ResolvConf
42 resolv_conf = defaultResolvConf { resolvTimeout = 10 * 1000 * 1000 }
44 -- | A list of common domains, there's no need to waste MX lookups on
45 -- these. This is a very limited list; I don't want to be in the
46 -- business of monitoring a million domains for MX record updates.
47 common_domains :: [Domain]
48 common_domains = map BS.pack [ "aol.com",
67 -- | Check whether the given domain has a valid MX record.
69 -- NULLMX (RFC7505) records consisting of a single period must not
70 -- be accepted. Moreover, the existence of a NULLMX must be reported
71 -- back to the caller because the whole point of a NULLMX is that
72 -- its existence should preempt an @A@ record check. We abuse the
73 -- return type for this, and return @Nothing@ in the event of a
74 -- NULLMX. Otherwise we return @Just True@ or @Just False@ to
75 -- indicate the existence (or not) of MX records.
77 -- RFC7505 states that a domain MUST NOT have any other MX records
78 -- if it has a NULLMX record. We enforce this. If you have a NULLMX
79 -- record and some other MX record, we consider the set invalid.
81 validate_mx :: Resolver -> Domain -> IO (Maybe Bool)
82 validate_mx resolver domain
83 | domain `elem` common_domains = return $ Just True
85 result <- lookupMX resolver domain
91 [] -> return $ Just False
92 _ -> if any (is_null) mxs
94 else return $ Just True
99 is_null :: (Domain,Int) -> Bool
100 is_null (mx,prio) = mx == nullmx && prio == 0
102 -- | Check whether the given domain has a valid A record.
103 validate_a :: Resolver -> Domain -> IO Bool
104 validate_a resolver domain
105 | domain `elem` common_domains = return True
107 result <- lookupA resolver domain
109 Right (_:_) -> return True
113 -- | Validate an email address by doing some simple syntax checks and
114 -- (if those fail) an MX lookup. We don't count an @A@ record as a mail
115 -- exchanger unless @accept_a@ is True. And even then, the existence
116 -- of a NULLMX record will preclude the acceptance of an @A@ record.
117 -- The domain @example.com@ is a great test case for this behavior.
118 validate :: Resolver -> Bool -> Bool -> Address -> IO (Address, Bool)
119 validate resolver accept_a rfc5322 address = do
120 let valid_syntax = validate_syntax rfc5322 address
121 if valid_syntax then do
122 let (_,domain) = parts address
123 mx_result <- validate_mx resolver domain
126 -- NULLMX, don't fall back to 'A' records under any
128 return (address, False)
131 then return (address, True)
135 a_result <- validate_a resolver domain
136 return (address, a_result)
138 return (address, False)
140 return (address, False)
147 -- Split stdin into lines, which should result in a list of addresses.
148 input <- BS.hGetContents stdin
149 let addresses = BS.lines input
151 -- And remove the empty ones.
152 let nonempty_addresses = filter (not . BS.null) addresses
154 rs <- makeResolvSeed resolv_conf
155 let validate' addr = withResolver rs $ \resolver ->
156 validate resolver accept_a rfc5322 addr
158 -- Construct a list of [IO (Address, Bool)]. The withResolver calls
159 -- are the ones that should be run in parallel.
160 let actions = map validate' nonempty_addresses
162 -- Run the lookup actions in parallel.
163 results <- parallelInterleaved actions
165 -- Filter the bad ones.
166 let valid_results = filter snd results
168 -- Output the results.
169 let valid_addresses = map fst valid_results
170 mapM_ (BS.hPutStrLn stdout) valid_addresses