addy-0.1.0.0: src/Addy/Internal/Validation.hs
-- |
--
-- Copyright:
-- This file is part of the package addy. It is subject to the license
-- terms in the LICENSE file found in the top-level directory of this
-- distribution and at:
--
-- https://code.devalot.com/open/addy
--
-- No part of this package, including this file, may be copied,
-- modified, propagated, or distributed except according to the terms
-- contained in the LICENSE file.
--
-- License: BSD-2-Clause
--
-- Internal data validation functions.
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
-- | Validate a single host name. Each host name in a domain name
-- (referred to as a /label/) must validate with this function.
--
-- RFC 2181 §11 clearly states that there are no restrictions placed
-- on which characters may appear in a label. However, due to legacy
-- issues we enforce the rule from RFC 952 §1 that disallows hyphens
-- as the first or last character of a label.
--
-- RFC 5322 §3.4.1 restricts the characters that may appear in the
-- domain component of an /email address/. Even though a DNS label
-- does not impose such restrictions, in order to be a valid email
-- address the label must only be composed of so-called @atext@
-- characters or @UTF8-non-ascii@ characters.
--
-- Finally, RFC 2181 §11 restricts the length of a label to 63 bytes
-- and the fully-qualified domain name to 255 bytes. RFC 6532 which
-- extends the email syntax to allow UTF-8 encoded Unicode characters
-- briefly states in §3.4 to continue using bytes, and not
-- characters. It also states that Unicode text should be normalized
-- (which we do).
--
-- @since 0.1.0.0
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'
)
-- | Validate a domain name.
--
-- The domain name is split into host names (labels) and each label is
-- validated with 'validateHostName'.
--
-- @since 0.1.0.0
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
-- | Validate and normalize the text content of the 'LocalPart' of an
-- email address.
--
-- RFC 3696 §3 restricts the length of the local part to a maximum of
-- 64 bytes. RFC 6532 extends the character set to include Unicode
-- characters but maintains the length measurement as bytes and not
-- characters.
--
-- @since 0.1.0.0
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
-- | Validate the content of a 'DisplayName'.
--
-- There does not appear to be a limit on the length of the display
-- name. For consistency and efficiency we limit it to 64 bytes, the
-- same as the local part.
--
-- @since 0.1.0.0
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
-- | Validate the 'Literal' content of a domain literal.
--
-- There does not appear to be a limit on the length of an address
-- literal but for consistency with DNS labels we limit them to 63
-- bytes.
--
-- @since 0.1.0.0
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'
-- | Validate the content of an 'AddressTag'. Uses the same rules as
-- 'validateLiteral'.
--
-- @since 0.1.0.0
validateAddressTag :: Text -> Validation (NonEmpty Error) AddressTag
validateAddressTag content = AT content <$ validateLiteral content
-- | Validate the content of a comment.
--
-- There does not appear to be a limit on the length of a comment.
-- For consistency and efficiency we limit it to 64 bytes, the same as
-- the local part.
--
-- @since 0.1.0.0
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
-- | Validate an entire 'EmailAddr'. This is used by the parser to
-- validate rules that are not encoded in the various component parsers.
--
-- @since 0.1.0.0
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
-- | Validate that the given text does not begin with the given prefix.
--
-- @since 0.1.0.0
validateNotPrefix :: Text -> Text -> Validation (NonEmpty Error) ()
validateNotPrefix prefix name =
failureIf (Text.isPrefixOf prefix name) (InvalidPrefixError prefix)
-- | Validate that the given text does not end with the given suffix.
--
-- @since 0.1.0.0
validateNotSuffix :: Text -> Text -> Validation (NonEmpty Error) ()
validateNotSuffix suffix name =
failureIf (Text.isSuffixOf suffix name) (InvalidSuffixError suffix)
-- | Validate that the text only contains characters for which the
-- given function returns true.
--
-- @since 0.1.0.0
validateAllowedChars :: (Char -> Bool) -> Text -> Validation (NonEmpty Error) ()
validateAllowedChars f t =
failureUnless (Text.all f t) (InvalidCharactersError $ Text.filter (not . f) t)
-- | Validate the length of the given text falls within the given
-- @min@ and @max@ values.
--
-- @since 0.1.0.0
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)