-- | -- -- 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 -- -- The module exports internal types along with their constructors. -- -- The rendering code relies on the newtype wrappers around 'Text' to -- keep out invalid characters. Prefer to use the official interface -- if possible. module Addy.Internal.Types ( Error (..), EmailAddr (..), displayName, localPart, domain, comments, DisplayName (..), LocalPart (..), Domain (..), _Domain, _DomainLiteral, DomainName (..), HostName (..), _HostNames, AddressLiteral (..), _IpAddressLiteral, _TaggedAddressLiteral, _AddressLiteral, AddressTag (..), Literal (..), Comment (..), _Comment, commentLoc, commentContent, CommentLoc (..), CommentContent (..), ) where import Control.Lens (Iso', Lens', Prism', iso, lens, prism') import qualified Data.Text as Text import GHC.TypeLits (KnownSymbol, Symbol, symbolVal) import Net.IP (IP) import Text.Show (Show (..), showParen, showString) -- | Potential validation errors. -- -- @since 0.1.0.0 data Error = -- | A component of an email address may not start with the -- recorded prefix text. InvalidPrefixError Text | -- | A component of an email address may not end with the recorded -- suffix text. InvalidSuffixError Text | -- | A component of an email address contains invalid characters. InvalidCharactersError Text | -- | A component of an email address does not meet the set length -- requirements. The values in this constructor are @min@, @max@, -- and @actual@. InvalidLengthError Int Int Int | -- | The input to the address decoder was not a valid email -- address and produced the recorded error message. ParserFailedError Text deriving (Show, Eq) -- | The representation of a complete email address. -- -- The parser preserves optional components such as the display name -- and comments. The rendering code can optionally include these -- optional elements when turning the address back into 'Text'. -- -- @since 0.1.0.0 data EmailAddr = EmailAddr { _displayName :: Maybe DisplayName, _localPart :: LocalPart, _domain :: Domain, _comments :: [Comment] } -- | Optional display name. Addresses in the @name-addr@ format -- from RFC 5322 allow descriptive text to precede the address. -- This is commonly used in email messages to list the name of the -- address' owner. -- -- @since 0.1.0.0 displayName :: Lens' EmailAddr (Maybe DisplayName) displayName = lens _displayName (\e d -> e {_displayName = d}) -- | The 'LocalPart' of an email address usually references the -- destination mailbox on the 'Domain' server. However, the -- content of the 'LocalPart' can only be understood by the -- receiving 'Domain'. -- -- @since 0.1.0.0 localPart :: Lens' EmailAddr LocalPart localPart = lens _localPart (\e l -> e {_localPart = l}) -- | The 'Domain' refers to the fully-qualified domain name that -- accepts mail for the associated 'LocalPart'. See the -- documentation for the 'Domain' type for more details. -- -- @since 0.1.0.0 domain :: Lens' EmailAddr Domain domain = lens _domain (\e d -> e {_domain = d}) -- | Addresses in both the @name-addr@ and @addr-spec@ formats -- support comments. -- -- @since 0.1.0.0 comments :: Lens' EmailAddr [Comment] comments = lens _comments (\e cs -> e {_comments = cs}) -- | Optional display name. Usually this is the name of the person -- who receives email at the associated address. -- -- > Display Name <example@example.com> -- -- @since 0.1.0.0 newtype DisplayName = DP { displayNameText :: Text } deriving newtype (Eq, Semigroup) deriving (Show) via RenamedShow "DisplayName" Text -- | The name of the mailbox on the associated 'Domain'. -- -- @since 0.1.0.0 newtype LocalPart = LP { localPartText :: Text } deriving newtype (Eq, Semigroup) deriving (Show) via RenamedShow "LocalPart" Text -- | A fully-qualified domain name /or/ an address literal. -- -- Most email addresses use a domain name. However, it's perfectly -- legal to use an 'AddressLiteral' instead. -- -- @since 0.1.0.0 data Domain = Domain DomainName | DomainLiteral AddressLiteral deriving (Show, Eq) -- | Prism for working with domain names. -- -- @since 0.1.0.0 _Domain :: Prism' Domain DomainName _Domain = prism' Domain ( \case Domain dn -> Just dn DomainLiteral {} -> Nothing ) -- | Prism for working with domain literals. -- -- @since 0.1.0.0 _DomainLiteral :: Prism' Domain AddressLiteral _DomainLiteral = prism' DomainLiteral ( \case Domain {} -> Nothing DomainLiteral dl -> Just dl ) -- | A fully-qualified domain name which is made up of a list of -- host names (labels) separated by dots. -- -- @since 0.1.0.0 newtype DomainName = DN { domainNameText :: Text } deriving newtype (Eq, Semigroup) deriving (Show) via RenamedShow "DomainName" Text -- | The name of one host component of a domain name. -- -- @since 0.1.0.0 newtype HostName = HN { hostNameText :: Text } deriving newtype (Eq, Semigroup) deriving (Show) via RenamedShow "HostName" Text -- | Iso for converting between domain names and a list of host names. -- -- >>> "gmail.uk.co" ^. _DomainName._HostNames & map (review _HostName) -- ["gmail","uk","co"] -- -- @since 0.1.0.0 _HostNames :: Iso' DomainName [HostName] _HostNames = iso (domainNameText >>> Text.splitOn "." >>> map HN) (map hostNameText >>> Text.intercalate "." >>> DN) -- | Address literals can be used instead of a domain name to direct -- mail to a specific IP address or other tagged address type. -- -- Example email addresses with address literals: -- -- > example@[127.0.0.1] -- > example@[IPv6:1111:2222:3333:4444:5555:6666:7777] -- > example@[Just-some-text] -- -- @since 0.1.0.0 data AddressLiteral = -- | A literal IP address as defined in RFC 5321 §4.1.3. The -- address can be in many formats so it is presented here in its -- parsed form. IpAddressLiteral IP | -- | RFC 5321 also defines a /general address literal/ where a -- /standardized tag/ precedes the address itself. The only -- information provided about the standardized tag is: -- -- > Standardized-tag MUST be specified in a -- > Standards-Track RFC and registered with IANA TaggedAddressLiteral AddressTag Literal | -- | RFC 5322 defines a @domain-literal@ as (roughly) a span of -- characters that are allowed in a domain name. The -- interpretation of those characters is left to \"separate -- documents\" such as RFC 5321. -- -- If an address literal cannot be parsed in one of the proceeding -- formats it is encoded as a 'Literal' value. AddressLiteral Literal deriving (Show, Eq) -- | Prism for working with IP address literals. -- -- @since 0.1.0.0 _IpAddressLiteral :: Prism' AddressLiteral IP _IpAddressLiteral = prism' IpAddressLiteral ( \case IpAddressLiteral ip -> Just ip TaggedAddressLiteral {} -> Nothing AddressLiteral {} -> Nothing ) -- | Prism for working with tagged address literals. -- -- @since 0.1.0.0 _TaggedAddressLiteral :: Prism' AddressLiteral (AddressTag, Literal) _TaggedAddressLiteral = prism' (uncurry TaggedAddressLiteral) ( \case IpAddressLiteral {} -> Nothing TaggedAddressLiteral tag body -> Just (tag, body) AddressLiteral {} -> Nothing ) -- | Prism for working with address literals. -- -- @since 0.1.0.0 _AddressLiteral :: Prism' AddressLiteral Literal _AddressLiteral = prism' AddressLiteral ( \case IpAddressLiteral {} -> Nothing TaggedAddressLiteral {} -> Nothing AddressLiteral lit -> Just lit ) -- | A tag that can be used with a 'TaggedAddressLiteral'. -- -- @since 0.1.0.0 newtype AddressTag = AT { addressTagText :: Text } deriving newtype (Eq, Semigroup) deriving (Show) via RenamedShow "AddressTag" Text -- | A literal address that can be used with a 'TaggedAddressLiteral' -- or 'AddressLiteral'. -- -- @since 0.1.0.0 newtype Literal = Lit { literalText :: Text } deriving newtype (Eq, Semigroup) deriving (Show) via RenamedShow "Literal" Text -- | A comment which may appear in an email address in a specific -- location. -- -- @since 0.1.0.0 data Comment = Comment CommentLoc CommentContent deriving (Show, Eq) -- | Prism for working with a 'Comment'. -- -- @since 0.1.0.0 _Comment :: Prism' Comment (CommentLoc, CommentContent) _Comment = prism' (uncurry Comment) (\(Comment loc cc) -> Just (loc, cc)) -- | Lens for working with comment locations. -- -- @since 0.1.0.0 commentLoc :: Lens' Comment CommentLoc commentLoc = lens (\(Comment loc _) -> loc) (\(Comment _ cc) loc -> Comment loc cc) -- | Lens for working with comment contents. -- -- @since 0.1.0.0 commentContent :: Lens' Comment CommentContent commentContent = lens (\(Comment _ cc) -> cc) (\(Comment loc _) cc -> Comment loc cc) -- | The location where a comment was parsed or where it should be -- rendered. -- -- @since 0.1.0.0 data CommentLoc = -- | Just before the 'DisplayName'. BeforeDisplayName | -- | Just after the 'DisplayName' but before the address. AfterDisplayName | -- | Before the 'LocalPart' of the address. BeforeLocalPart | -- | After the 'Domain'. AfterDomain | -- | After the complete address. AfterAddress deriving (Show, Eq) -- | Text that can appear in a comment. -- -- @since 0.1.0.0 newtype CommentContent = CC { commentContentText :: Text } deriving newtype (Eq, Semigroup) deriving (Show) via RenamedShow "CommentContent" Text -- | Newtype wrapper for deriving 'Show' instances that lie about the -- name of the constructor. newtype RenamedShow (n :: Symbol) a = RS a instance (Show a, KnownSymbol n) => Show (RenamedShow n a) where showsPrec d (RS x) = showParen (d > 10) $ showString (symbolVal (Proxy :: Proxy n) <> " ") . showsPrec d x