module EmailAddress ( Address, parts, validate_syntax, -- * Test exports email_address_tests ) where import qualified Data.ByteString.Char8 as BS ( ByteString, break, empty, length, pack, tail ) import Text.Email.Validate ( isValid ) import Test.HUnit ( assertEqual ) import Test.Tasty ( TestTree, testGroup ) import Test.Tasty.HUnit ( (@?=), testCase ) import Text.Regex.PCRE.Light ( anchored, caseless, compile, dollar_endonly, match, utf8 ) type Address = BS.ByteString type LocalPart = BS.ByteString type DomainPart = BS.ByteString -- | Split an address into local/domain parts. -- -- Examples: -- -- >>> let addr = BS.pack "user@example.com" -- >>> parts addr -- ("user","example.com") -- parts :: Address -> (LocalPart, DomainPart) parts address = (before, after) where break_func = (== '@') (before, rest) = BS.break break_func address after = if rest == BS.empty then BS.empty else BS.tail rest -- | Check that the lengths of the local/domain parts are within spec. -- -- Examples: -- -- >>> let jr = (replicate 64 'a') ++ "@" ++ (replicate 255 'x') -- >>> let just_right = BS.pack jr -- >>> validate_length just_right -- True -- >>> let too_long = BS.pack (jr ++ "x") -- >>> validate_length too_long -- False -- validate_length :: Address -> Bool validate_length address = (BS.length localpart <= 64) && (BS.length domain <= 255) where (localpart, domain) = parts address -- | Validate an email address against a simple regex. This should -- catch common addresses but disallows a lot of (legal) weird stuff. -- -- Examples: -- -- >>> let addr = BS.pack "user@example.com" -- >>> validate_regex addr -- True -- >>> let bad_addr = BS.pack "user@[]example.com" -- >>> validate_regex bad_addr -- False -- validate_regex :: Address -> Bool validate_regex address = case matches of Nothing -> False _ -> True where regex_str = "(\\w+)([\\w\\-\\.]*)@(([a-z0-9\\-]+\\.)+)[a-z]{2,4}$" regex_bs = BS.pack regex_str regex = compile regex_bs [anchored, caseless, dollar_endonly, utf8] matches = match regex address [] -- | Validate the syntax of an email address by checking its length -- and validating it against either a simple regex or RFC5322, -- depending on the --rfc5322 flag. -- validate_syntax :: Bool -> Address -> Bool validate_syntax rfc5322 address = (validate_length address) && if rfc5322 then isValid address else validate_regex address -- * Tasty Tests good_addresses :: [Address] good_addresses = map BS.pack [ "phil@hotmail.com", "philq23562@hotmail.com", "gsdfg22-2_22@hot-mail.com", "bill.w@sub.domain.com", "paul@sub.domain.co.uk", "someone_45@someplace.info" ] bad_addresses :: [Address] bad_addresses = map BS.pack [ -- Bad, but not caught by email-validate-0.0.1. -- "badunderscore@dom_ain.com", "(fail)@domain.com", "no spaces@domain.com", ".beginswith@a-dot.com", "a", "a.com", "@b.com", "b@", (replicate 65 'a') ++ "@" ++ "domain.com", "abcdefg@" ++ (replicate 253 'a') ++ ".com", (replicate 100 'a') ++ "@" ++ (replicate 300 'a') ++ ".com" ] unsupported_addresses :: [Address] unsupported_addresses = map BS.pack [ "ok!char@domain.com", "ok#char@domain.com", "ok$char@domain.com", "ok'char@domain.com", "ok*char@domain.com", "ok+char@domain.com", "ok/char@domain.com", "ok=char@domain.com", "ok?char@domain.com", "ok`char@domain.com", "ok{char@domain.com", "ok|char@domain.com", "ok}char@domain.com", "ok~char@domain.com", "tom.phillips@[127.0.0.1]", "bad%char@domain.com", "bad^char@domain.com" ] test_good_addresses :: TestTree test_good_addresses = testCase desc $ actual @?= expected where desc = "Good addresses are accepted" expected = True actual = all (validate_syntax False) good_addresses test_good_addresses_rfc :: TestTree test_good_addresses_rfc = testCase desc $ actual @?= expected where desc = "Good addresses are accepted with --rfc5322" expected = True actual = all (validate_syntax True) good_addresses test_bad_addresses :: TestTree test_bad_addresses = testCase desc $ actual @?= expected where desc = "Bad addresses are not accepted" expected = True actual = all (not . (validate_syntax False)) bad_addresses test_bad_addresses_rfc :: TestTree test_bad_addresses_rfc = testCase desc $ assertEqual desc expected actual where desc = "Bad addresses are not accepted with --rfc5322" expected = True actual = all (not . (validate_syntax True)) bad_addresses test_unsupported_addresses :: TestTree test_unsupported_addresses = testCase desc $ actual @?= expected where desc = "Unsupported addresses are not accepted" expected = True actual = all (not . (validate_syntax False)) unsupported_addresses test_unsupported_addresses_rfc :: TestTree test_unsupported_addresses_rfc = testCase desc $ actual @?= expected where desc = "Unsupported addresses are accepted with --rfc5322" expected = True actual = all (validate_syntax True) unsupported_addresses email_address_tests :: TestTree email_address_tests = testGroup "EmailAddress Tests" [ test_good_addresses, test_good_addresses_rfc, test_bad_addresses, test_bad_addresses_rfc, test_unsupported_addresses, test_unsupported_addresses_rfc ]