module Addy.Internal.Validation
( validateHostName,
validateDomainName,
validateLocalPart,
validateDisplayName,
validateLiteral,
validateAddressTag,
validateCommentContent,
validateEmailAddr,
)
where
import Addy.Internal.Char
import Addy.Internal.Types
import qualified Data.ByteString as ByteString
import qualified Data.Text as Text
import qualified Data.Text.ICU as ICU
import Validation
validateHostName :: Text -> Validation (NonEmpty Error) HostName
validateHostName content =
let content' = Text.toLower (ICU.normalize ICU.NFC content)
in HN content'
<$ ( validateNotPrefix "-" content'
*> validateNotSuffix "-" content'
*> validateAllowedChars atext content'
*> validateLength 1 63 content'
)
validateDomainName :: Text -> Validation (NonEmpty Error) DomainName
validateDomainName name =
fromHostList <$> (validateLength 1 255 name *> validHostList)
where
validHostList :: Validation (NonEmpty Error) [HostName]
validHostList =
foldr
( \h hs -> (:) <$> validateHostName h <*> hs
)
(pure [])
(Text.splitOn "." name)
fromHostList :: [HostName] -> DomainName
fromHostList hs =
map coerce hs
& Text.intercalate "."
& DN
validateLocalPart ::
Text -> Validation (NonEmpty Error) LocalPart
validateLocalPart content =
let content' = ICU.normalize ICU.NFC content
in LP content'
<$ ( validateLength 1 64 content'
*> validateAllowedChars allowedChar content'
)
where
allowedChar :: Char -> Bool
allowedChar c = atext c || c == '.' || qtext c || quotedPair c
validateDisplayName :: Text -> Validation (NonEmpty Error) DisplayName
validateDisplayName content =
DP content
<$ ( validateLength 1 64 content
*> validateAllowedChars allowedChar content
)
where
allowedChar :: Char -> Bool
allowedChar c = atext c || qtext c || quotedPair c
validateLiteral :: Text -> Validation (NonEmpty Error) Literal
validateLiteral content =
Lit content
<$ ( validateLength 1 63 content
*> validateAllowedChars allowedChar content
)
where
allowedChar :: Char -> Bool
allowedChar c = dtext c || wsp c || c == '\r' || c == '\n'
validateAddressTag :: Text -> Validation (NonEmpty Error) AddressTag
validateAddressTag content = AT content <$ validateLiteral content
validateCommentContent :: Text -> Validation (NonEmpty Error) CommentContent
validateCommentContent content =
CC content
<$ ( validateLength 1 64 content
*> validateAllowedChars allowedChar content
)
where
allowedChar :: Char -> Bool
allowedChar c = ctext c || quotedPair c
validateEmailAddr :: EmailAddr -> Validation (NonEmpty Error) EmailAddr
validateEmailAddr EmailAddr {..} =
EmailAddr
<$> displayNameV
<*> validateLocalPart (localPartText _localPart)
<*> domainV
<*> commentsV
where
displayNameV :: Validation (NonEmpty Error) (Maybe DisplayName)
displayNameV = case _displayName of
Nothing -> pure Nothing
Just (DP t) -> Just <$> validateDisplayName t
domainV :: Validation (NonEmpty Error) Domain
domainV = case _domain of
Domain (DN t) -> Domain <$> validateDomainName t
DomainLiteral lit -> DomainLiteral <$> addrLiteralV lit
addrLiteralV :: AddressLiteral -> Validation (NonEmpty Error) AddressLiteral
addrLiteralV = \case
IpAddressLiteral ip ->
pure (IpAddressLiteral ip)
TaggedAddressLiteral (AT at) (Lit lit) ->
TaggedAddressLiteral <$> validateAddressTag at <*> validateLiteral lit
AddressLiteral (Lit t) ->
AddressLiteral <$> validateLiteral t
commentsV :: Validation (NonEmpty Error) [Comment]
commentsV =
foldr
( \(Comment loc (CC t)) cs ->
(:) . Comment loc <$> validateCommentContent t <*> cs
)
(pure [])
_comments
validateNotPrefix :: Text -> Text -> Validation (NonEmpty Error) ()
validateNotPrefix prefix name =
failureIf (Text.isPrefixOf prefix name) (InvalidPrefixError prefix)
validateNotSuffix :: Text -> Text -> Validation (NonEmpty Error) ()
validateNotSuffix suffix name =
failureIf (Text.isSuffixOf suffix name) (InvalidSuffixError suffix)
validateAllowedChars :: (Char -> Bool) -> Text -> Validation (NonEmpty Error) ()
validateAllowedChars f t =
failureUnless (Text.all f t) (InvalidCharactersError $ Text.filter (not . f) t)
validateLength :: Int -> Int -> Text -> Validation (NonEmpty Error) ()
validateLength minL maxL t =
let bytes = ByteString.length (encodeUtf8 t)
in failureIf
(bytes < minL || bytes > maxL)
(InvalidLengthError minL maxL bytes)