addy-0.1.0.0: src/Addy.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
--
-- Email addressed are complicated, really complicated. This library
-- supports all standardized forms of email addresses, including those
-- with UTF-8 encoded Unicode characters. The standards used by this
-- library include:
--
-- * RFC 1123: Requirements for Internet Hosts -- Application and Support
-- * RFC 2181: Clarifications to the DNS Specification
-- * RFC 3696: Application Techniques for Checking and Transformation of Names
-- * RFC 5321: Simple Mail Transfer Protocol
-- * RFC 5322: Internet Message Format
-- * RFC 6531: SMTP Extension for Internationalized Email
-- * RFC 6532: Internationalized Email Headers
module Addy
( -- * How to use this library
-- $use
-- * Decoding and encoding
decode,
decodeLenient,
encode,
encodeFull,
-- * Email addresses
EmailAddr,
emailAddr,
emailAddrLit,
displayName,
localPart,
domain,
comments,
-- * Display name
DisplayName,
_DisplayName,
validateDisplayName,
-- * Local part
LocalPart,
_LocalPart,
validateLocalPart,
-- * Domain part
Domain (..),
_Domain,
_DomainLiteral,
DomainName,
_DomainName,
validateDomainName,
-- * Host names
HostName,
_HostNames,
_HostName,
validateHostName,
-- * Address literals
AddressLiteral (..),
_IpAddressLiteral,
_TaggedAddressLiteral,
_AddressLiteral,
AddressTag,
_AddressTag,
validateAddressTag,
Literal,
_Literal,
validateLiteral,
-- * Comments
Comment (..),
_Comment,
commentLoc,
commentContent,
CommentLoc (..),
CommentContent,
_CommentContent,
validateCommentContent,
)
where
import Addy.Internal.Parser as P
import Addy.Internal.Render as R
import Addy.Internal.Types
import Addy.Internal.Validation
import Control.Lens (Prism', prism')
import Validation.Combinators (successToMaybe)
-- | Decode an email address.
--
-- @since 0.1.0.0
decode :: Text -> Either (NonEmpty Error) EmailAddr
decode = P.parseWithMode P.Strict
-- | Decode an email address, allowing obsolete characters. The
-- obsolete characters are parsed but not included in the output.
--
-- This is useful for exacting email addresses from mail messages but
-- should not be used to validate user input.
--
-- >>> Addy.decode "my . email . addy@(WTF)example.com"
-- Left (ParserFailedError "local part > quoted content > '\"': Failed reading: satisfy" :| [])
--
-- >>> Addy.decodeLenient "my . email . addy@(WTF)example.com"
-- Right (EmailAddr "my.email.addy@example.com (WTF)")
--
-- @since 0.1.0.0
decodeLenient :: Text -> Either (NonEmpty Error) EmailAddr
decodeLenient = P.parseWithMode P.Lenient
-- | Encode an email address as text. This function produces the
-- short form of an email address. That is, just the 'LocalPart' and
-- the 'Domain' separated by @\@@.
--
-- @since 0.1.0.0
encode :: EmailAddr -> Text
encode = R.renderToText R.Short
-- | Encode a complete email address to text, including the optional
-- display name and any comments.
--
-- @since 0.1.0.0
encodeFull :: EmailAddr -> Text
encodeFull = R.renderToText R.Full
-- | Build an 'EmailAddr' from a 'LocalPart' and 'DomainName'.
--
-- @since 0.1.0.0
emailAddr :: LocalPart -> DomainName -> EmailAddr
emailAddr l d = EmailAddr Nothing l (Domain d) []
-- | Build an 'EmailAddr' from a 'LocalPart' and an 'AddressLiteral'.
--
-- @since 0.1.0.0
emailAddrLit :: LocalPart -> AddressLiteral -> EmailAddr
emailAddrLit l d = EmailAddr Nothing l (DomainLiteral d) []
-- | Prism for working with display names.
--
-- > import Control.Lens ((^?), review)
--
-- To convert text into a 'DisplayName' with content validation:
--
-- >>> "Some Text" ^? _DisplayName
-- Just (DisplayName "Some Text")
-- >>> "Some\nText" ^? _DisplayName
-- Nothing -- Validation failed.
--
-- To access the text content of a 'DisplayName':
--
-- >>> review _DisplayName someDisplayName
-- "Some Text"
--
-- Uses 'validateDisplayName' to perform validation.
--
-- @since 0.1.0.0
_DisplayName :: Prism' Text DisplayName
_DisplayName =
prism'
displayNameText
(validateDisplayName >>> successToMaybe)
-- | Prism for working with the local part of an email address.
--
-- > import Control.Lens ((^?), review)
--
-- To convert text to a 'LocalPart' with content validation:
--
-- >>> "cockroach+mouse" ^? _LocalPart
-- Just (LocalPart "cockroach+mouse")
-- >>> "cockroach\nmouse" ^? _LocalPart
-- Nothing -- Validation failed.
--
-- To access the text content of a 'LocalPart':
--
-- >>> review _LocalPart someLocalPart
-- "cockamouse"
--
-- Uses 'validateLocalPart' to perform validation.
--
-- @since 0.1.0.0
_LocalPart :: Prism' Text LocalPart
_LocalPart =
prism'
localPartText
(validateLocalPart >>> successToMaybe)
-- | Prism for working with domain names.
--
-- > import Control.Lens ((^?), review)
--
-- To convert text to a 'DomainName' with validation:
--
-- >>> "gmail.com" ^? _DomainName
-- Just (DomainName "gmail.com")
-- >>> "too.many.dots." ^? _DomainName
-- Nothing
--
-- To access the text content of a 'DomainName':
--
-- >>> review _DomainName someDomainName
-- "gmail.com"
--
-- Uses 'validateDomainName' to perform validation.
--
-- @since 0.1.0.0
_DomainName :: Prism' Text DomainName
_DomainName =
prism'
domainNameText
(validateDomainName >>> successToMaybe)
-- | Prism for working with host names (DNS labels).
--
-- > import Control.Lens ((^?), review)
--
-- To convert text to a host name with validation:
--
-- >>> "com" ^? _HostName
-- Just (HostName "com")
-- >>> "com." ^? _HostName
-- Nothing -- Validation failed.
--
-- To access the text content of a 'HostName':
--
-- >>> review _HostName someHostName
-- "com"
--
-- Uses 'validateHostName' to perform validation.
--
-- @since 0.1.0.0
_HostName :: Prism' Text HostName
_HostName =
prism'
hostNameText
(validateHostName >>> successToMaybe)
-- | Prism for working with the 'AddressTag' for an 'AddressLiteral'.
--
-- > import Control.Lens ((^?), review)
--
-- To convert text to an address tag with validation:
--
-- >>> "IPv6" ^? _AddressTag
-- Just (AddressTag "IPv6")
-- >>> "[IPv6]" ^? _AddressTag
-- Nothing -- Validation failed.
--
-- To access the text content of an 'AddressTag':
--
-- >>> review _AddressTag someTag
-- "tag"
--
-- Uses 'validateAddressTag' to perform validation.
--
-- @since 0.1.0.0
_AddressTag :: Prism' Text AddressTag
_AddressTag =
prism'
addressTagText
(validateAddressTag >>> successToMaybe)
-- | Prism for working with the literal text of an address literal.
--
-- > import Control.Lens ((^?), review)
--
-- To convert text to an address literal with validation:
--
-- >>> "127.0.0.1" ^? _Literal
-- Just (Literal "127.0.0.1")
-- >>> "[]" ^? _Literal
-- Nothing -- Validation failed.
--
-- To access the text content of a 'Literal':
--
-- >>> review _Literal someLiteral
-- "127.0.0.1"
--
-- Uses 'validateLiteral' to perform validation.
--
-- @since 0.1.0.0
_Literal :: Prism' Text Literal
_Literal =
prism'
literalText
(validateLiteral >>> successToMaybe)
-- | Prism for working with the text content of a comment.
--
-- > import Control.Lens ((^?), review)
--
-- To convert text to a comment with validation:
--
-- >>> "best email ever" ^? _CommentContent
-- Just (CommentContent "best email ever")
-- >>> "\n" ^? _CommentContent
-- Nothing
--
-- To access the text content of the comment:
--
-- >>> review _CommentContent someComment
-- "super"
--
-- Uses 'validateCommentContent' to perform validation.
--
-- @since 0.1.0.0
_CommentContent ::
Prism' Text CommentContent
_CommentContent =
prism'
commentContentText
(validateCommentContent >>> successToMaybe)
-- $use
--
-- == Importing
--
-- This library is designed to be imported qualified:
--
-- > import qualified Addy
--
-- == Decoding addresses
--
-- To decode (parse) an email address from text:
--
-- >>> Addy.decode "example@example.com"
-- Right (EmailAddr "example@example.com")
--
-- >>> Addy.decode "我買@屋企.香港"
-- Right (EmailAddr "\25105\36023@\23627\20225.\39321\28207")
--
-- >>> Addy.decode "Mary Smith <mary@example.net> (hi there!)"
-- Right (EmailAddr "Mary Smith <mary@example.net> (hi there!)")
--
-- >>> Addy.decode "example@[127.0.0.1]"
-- Right (EmailAddr "example@[127.0.0.1]")
--
-- After decoding, an address is automatically normalized by performing
-- NFC normalization and down-casing the domain name:
--
-- >>> Addy.decode "My.Email.Addy@ExAmPlE.COM"
-- Right (EmailAddr "My.Email.Addy@example.com")
--
-- == Encoding addresses
--
-- Turning an email address back to text is just as easy:
--
-- >>> Addy.encode address
-- "example@example.com"
--
-- If an address has an optional display name or comments you can
-- render those with the 'encodeFull' function.
--
-- >>> :{
-- Addy.decode "Mary Smith <mary@example.net> (hi there!)"
-- & second Addy.encodeFull
-- :}
-- Right "Mary Smith <mary@example.net> (hi there!)"
--
-- == Creating addresses
--
-- In order to prevent invalid email addresses from being created this
-- library uses @newtype@ wrappers and does not export the data
-- constructors. Therefore you'll need to use the smart constructor
-- approach using the 'emailAddr' function.
--
-- If you want to work with the validation functions directly we
-- recommend 'Applicative' syntax:
--
-- >>> :{
-- Addy.emailAddr
-- <$> Addy.validateLocalPart "pjones"
-- <*> Addy.validateDomainName "devalot.com"
-- :}
-- Success (EmailAddr "pjones@devalot.com")
--
-- Prisms for the @newtype@ wrappers are provided if you want to use optics:
--
-- >>> :{
-- Addy.emailAddr
-- <$> "pjones" ^? Addy._LocalPart
-- <*> "devalot.com" ^? Addy._DomainName
-- :}
-- Just (EmailAddr "pjones@devalot.com")
--
-- == Optics
--
-- Lens and prisms are provided to make working with email addresses easier:
--
-- > import qualified Addy
-- > import Control.Lens
--
-- >>> Addy.decode "example@example.com" ^? _Right . Addy.domain
-- Just (Domain (DomainName "example.com"))
--
-- >>> Addy.decode "example@example.com"
-- ^? _Right . Addy.domain . Addy._Domain . Addy._HostNames
-- Just [HostName "example",HostName "com"]
--
-- >>> Addy.decode "example@example.com"
-- ^.. _Right . Addy.domain . Addy._Domain
-- . Addy._HostNames . traversed . re _HostName
-- ["example","com"]
--
-- == A word about address literals
--
-- Believe it or not, this is a completely valid email address:
--
-- >>> Addy.decode "example@[what's my domain name?]"
-- ^? _Right . Addy.domain
-- Just (DomainLiteral (AddressLiteral (Literal "what's my domain name?")))
--
-- If you're working with email messages it might be useful to capture
-- these address literals, especially if you know how to interpret
-- them. However, if you're validating user input you probably don't
-- want to allow these.
--
-- >>> Addy.decode "e@[127.0.0.1]" ^? _Right
-- >>= failover (Addy.domain . Addy._Domain) id
-- Nothing
--
-- >>> Addy.decode "example@example.com" ^? _Right
-- >>= failover (Addy.domain . Addy._Domain) id
-- Just (EmailAddr "example@example.com")