{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} module Text.Html.Email.Validate ( -- * Validating isValidEmail -- * Parsing , EmailAddress(..) , emailToText , parseEmail , emailParser ) where import Control.Applicative import Control.Monad (when) import Data.Text (Text, intercalate) import qualified Data.Text as T import Data.Attoparsec.Text import Data.Monoid ((<>)) import qualified Text.Read as Read import Data.Data (Data, Typeable) import GHC.Generics (Generic) import qualified Text.Show.Text as TS -- | Represents an email address data EmailAddress = EmailAddress { localPart :: Text , domainPart :: Text } deriving (Eq, Ord, Data, Typeable, Generic) instance Show EmailAddress where show = TS.toString . TS.showb instance Read EmailAddress where readListPrec = Read.readListPrecDefault readPrec = Read.parens $ do text <- Read.readPrec either (const Read.pfail) return $ parseOnly emailParser text instance TS.Show EmailAddress where showb EmailAddress{..} = TS.fromText localPart <> TS.singleton '@' <> TS.fromText domainPart -- | Convert to text. Note that 'EmailAddress' has an instance of 'TS.Show' from -- 'text-show', you might want to use it instead. -- -- >>> emailToText $ EmailAddress "name" "example.com" -- "name@example.com emailToText :: EmailAddress -> Text emailToText = TS.show -- | Validates given email. Email shouldn't have trailing or preceding spaces -- -- >>> :set -XOverloadedStrings -- >>> isValidEmail "name@example.com" -- True -- >>> isValidEmail "name@example..com" -- False isValidEmail :: Text -> Bool isValidEmail = either (const False) (const True) . parseEmail -- | Parce an email. Error messages aren't very helpful. parseEmail :: Text -> Either String EmailAddress parseEmail = parseOnly emailParser -- | Attoparsec parser. emailParser :: Parser EmailAddress emailParser = EmailAddress <$> (local <* char '@') <*> (domain <* endOfInput) local :: Parser Text local = takeWhile1 (inClass "A-Za-z0-9!#$%&'*+/=?^_`{|}~.-") domain :: Parser Text domain = intercalate "." <$> label `sepBy1` char '.' label :: Parser Text label = do lbl <- intercalate "-" <$> takeWhile1 (inClass "A-Za-z0-9") `sepBy1` char '-' when (T.length lbl > 63) $ fail "Label is too long" return lbl