packages feed

addy (empty) → 0.1.0.0

raw patch · 17 files changed

+2771/−0 lines, 17 filesdep +addydep +aesondep +attoparsecsetup-changed

Dependencies added: addy, aeson, attoparsec, base, bytestring, hedgehog, http-types, ip, lens, relude, tasty, tasty-hedgehog, tasty-hunit, text, text-icu, validation-selective

Files

+ CHANGES.md view
@@ -0,0 +1,7 @@+Release History+===============++[0.1.0] - 2020-05-08+--------------------++  * Initial release
+ LICENSE view
@@ -0,0 +1,26 @@+Copyright (c) 2020 Peter J. Jones <pjones@devalot.com>+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++1. Redistributions of source code must retain the above copyright+   notice, this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright+   notice, this list of conditions and the following disclaimer in the+   documentation and/or other materials provided with the+   distribution.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,38 @@+Addy: a modern library for working with email addresses+=======================================================++[![addy](https://circleci.com/gh/pjones/addy.svg?style=shield)](https://app.circleci.com/github/pjones/addy/pipelines)+[![Hackage](https://img.shields.io/hackage/v/addy?logo=haskell)](https://hackage.haskell.org/package/addy)+[![GitHub](https://img.shields.io/github/license/pjones/addy)](https://github.com/pjones/addy/blob/master/LICENSE)++A full-featured library for parsing, validating, and rendering email+addresses.++Decoding+--------++```haskell+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]")+```++Encoding+--------++```haskell+Addy.encode address+-- "example@example.com"++Addy.decode "Mary Smith <mary@example.net> (hi there!)"+  & second Addy.encodeFull+-- Right "Mary Smith <mary@example.net> (hi there!)"+```
+ Setup.hs view
@@ -0,0 +1,16 @@+-- |+--+-- 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+import Distribution.Simple+main = defaultMain
+ addy.cabal view
@@ -0,0 +1,126 @@+cabal-version: 2.2++--------------------------------------------------------------------------------+name:          addy+version:       0.1.0.0+synopsis:      A full-featured library for parsing, validating, and rendering email addresses+license:       BSD-2-Clause+license-file:  LICENSE+author:        Peter Jones <pjones@devalot.com>+maintainer:    Peter Jones <pjones@devalot.com>+copyright:     Copyright (c) 2019-2020 Peter Jones+homepage:      https://github.com/pjones/addy+bug-reports:   https://github.com/pjones/addy/issues+category:      Text+tested-with:   GHC == 8.6.5, GHC == 8.8.3+build-type:    Simple+description:+  A modern library for working with email addresses.  This package can+  be used to validate user input or handle various flavors of+  addresses that appear in places such as mail messages.+  .+  Email addresses can be decoded, manipulated, then rendered back to+  text using the simple @local-part\@domain@ format or the more+  complete syntax that allows for display names and comments.+  .+  Comprehensive documentation can be found in the "Addy" module.++--------------------------------------------------------------------------------+extra-source-files:+  README.md+  CHANGES.md+  test/isemail.json+  test/README.md++--------------------------------------------------------------------------------+common options+  default-language: Haskell2010++  default-extensions:+    DataKinds+    DeriveAnyClass+    DeriveFoldable+    DeriveFunctor+    DeriveGeneric+    DeriveTraversable+    DerivingVia+    GeneralizedNewtypeDeriving+    KindSignatures+    LambdaCase+    MultiParamTypeClasses+    MultiWayIf+    NamedFieldPuns+    NumericUnderscores+    OverloadedStrings+    RankNTypes+    RecordWildCards+    ScopedTypeVariables+    StandaloneDeriving+    TupleSections+    TypeApplications++  ghc-options:+    -Wall+    -Wno-name-shadowing+    -Werror=incomplete-record-updates+    -Werror=incomplete-uni-patterns+    -Werror=missing-home-modules+    -Widentities+    -Wmissing-export-lists+    -Wredundant-constraints++--------------------------------------------------------------------------------+common dependencies+  build-depends:+    attoparsec >= 0.13 && < 0.14,+    base >= 4.9 && < 5.0,+    bytestring >= 0.10 && < 0.11,+    ip >= 1.5.1 && < 1.8,+    lens >= 4.17 && < 5.0,+    relude >= 0.6 && < 0.7,+    text >= 1.2 && < 1.3,+    text-icu >= 0.7 && < 0.8,+    validation-selective >= 0.1 && < 0.2++  mixins: base hiding (Prelude)+        , relude (Relude as Prelude)++--------------------------------------------------------------------------------+library+  import: options, dependencies+  hs-source-dirs: src+  exposed-modules:+    Addy+    Addy.Internal.Char+    Addy.Internal.Parser+    Addy.Internal.Render+    Addy.Internal.Types+    Addy.Internal.Validation++--------------------------------------------------------------------------------+test-suite test+  import: options, dependencies+  type: exitcode-stdio-1.0+  hs-source-dirs: test+  main-is: Main.hs+  other-modules:+    ParserTest+    RenderTest+    TestData+  build-depends:+    addy,+    aeson >= 1.4,+    hedgehog >= 1.0 && < 1.1,+    http-types >= 0.12,+    tasty >= 1.1 && < 1.3,+    tasty-hedgehog >= 1.0 && < 1.1,+    tasty-hunit >= 0.10 && < 0.11++--------------------------------------------------------------------------------+-- test-suite doctests+--   import: options, dependencies+--   type: exitcode-stdio-1.0+--   ghc-options: -threaded+--   hs-source-dirs: test/docs+--   main-is: Main.hs+--   build-depends: doctest >= 0.16
+ src/Addy.hs view
@@ -0,0 +1,437 @@+-- |+--+-- 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")
+ src/Addy/Internal/Char.hs view
@@ -0,0 +1,224 @@+-- |+--+-- 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 functions representing character classes in email+-- addresses.+--+-- Obsolete characters are only supported in+-- 'Addy.Internal.Parser.Lenient' mode and are filtered out after+-- parsing.+module Addy.Internal.Char+  ( utf8NonAscii,+    obsNoWsCtl,+    wsp,+    vchar,+    atext,+    dtext,+    ctext,+    ctextObs,+    qtext,+    qtextObs,+    quotedPair,+    quotedPairObs,+  )+where++import Data.Char++-- | Is a character in the @UTF8-non-ascii@ class from RFC 6532?+--+-- @since 0.1.0.0+utf8NonAscii :: Char -> Bool+utf8NonAscii c = ord c >= 0xc2 && isPrint c++-- | Obsolete control characters.+--+-- > obs-NO-WS-CTL   =   %d1-8 /            ; US-ASCII control+-- >                     %d11 /             ;  characters that do not+-- >                     %d12 /             ;  include the carriage+-- >                     %d14-31 /          ;  return, line feed, and+-- >                     %d127              ;  white space characters+--+-- @since 0.1.0.0+obsNoWsCtl :: Char -> Bool+obsNoWsCtl = ord >>> go+  where+    go n =+      (n >= 1 && n <= 8)+        || n == 11+        || n == 12+        || (n >= 14 && n <= 31)+        || n == 127++-- | Whitepace.+--+-- @since 0.1.0.0+wsp :: Char -> Bool+wsp c = c == ' ' || c == '\t'++-- | RFC 5234: Visible character.+--+-- > VCHAR          =  %x21-7E+-- >                        ; visible (printing) characters+--+--  RFC 6532 §3.2+--+-- > VCHAR   =/  UTF8-non-ascii+--+-- @since 0.1.0.0+vchar :: Char -> Bool+vchar c = vchar' (ord c) || utf8NonAscii c+  where+    vchar' n = n >= 0x21 && n <= 0x7e++-- | RFC 5322 §3.2.3+--+-- > atext =   ALPHA / DIGIT /    ; Printable US-ASCII+-- >           "!" / "#" /        ;  characters not including+-- >           "$" / "%" /        ;  specials.  Used for atoms.+-- >           "&" / "'" /+-- >           "*" / "+" /+-- >           "-" / "/" /+-- >           "=" / "?" /+-- >           "^" / "_" /+-- >           "`" / "{" /+-- >           "|" / "}" /+-- >           "~"+--+--  RFC 6532 §3.2+--+-- > atext =/  UTF8-non-ascii+--+-- @since 0.1.0.0+atext :: Char -> Bool+atext c =+  isAlphaNum c+    || utf8NonAscii c+    || c == '!'+    || c == '#'+    || c == '$'+    || c == '%'+    || c == '&'+    || c == '\''+    || c == '*'+    || c == '+'+    || c == '-'+    || c == '/'+    || c == '='+    || c == '?'+    || c == '^'+    || c == '_'+    || c == '`'+    || c == '{'+    || c == '|'+    || c == '}'+    || c == '~'++-- | RFC 5322 @dtext@.+--+-- > dtext           =   %d33-90 /          ; Printable US-ASCII+-- >                     %d94-126 /         ;  characters not including+-- >                     obs-dtext          ;  "[", "]", or "\"+-- > obs-dtext       =   obs-NO-WS-CTL / quoted-pair+--+--  RFC 6532 §3.2+--+-- > dtext   =/  UTF8-non-ascii+-- @since 0.1.0.0+dtext :: Char -> Bool+dtext c = asciidtext (ord c) || utf8NonAscii c+  where+    asciidtext n =+      (n >= 33 && n <= 90)+        || (n >= 94 && n <= 126)++-- | RFC 5322 @ctext@.+--+-- > ctext           =   %d33-39 /          ; Printable US-ASCII+-- >                     %d42-91 /          ;  characters not including+-- >                     %d93-126 /         ;  "(", ")", or "\"+-- >                     obs-ctext+-- >+-- > obs-ctext       =   obs-NO-WS-CTL+--+-- RFC 6532 §3.2+--+-- > ctext   =/  UTF8-non-ascii+--+-- @since 0.1.0.0+ctext :: Char -> Bool+ctext c = asciictext (ord c) || utf8NonAscii c+  where+    asciictext n =+      (n >= 33 && n <= 39)+        || (n >= 42 && n <= 91)+        || (n >= 93 && n <= 126)++-- | Obsolete @ctext@.+--+-- > obs-ctext = obs-NO-WS-CTL+--+-- @since 0.1.0.0+ctextObs :: Char -> Bool+ctextObs = obsNoWsCtl++-- | Characters that can appear in a quoted string.+--+-- RFC 5322 §3.2.4:+--+-- > qtext =   %d33 /             ; Printable US-ASCII+-- >           %d35-91 /          ;  characters not including+-- >           %d93-126 /         ;  "\" or the quote character+-- >           obs-qtext+--+-- RFC 6532 §3.2:+--+-- > qtext =/ UTF8-non-ascii+-- @since 0.1.0.0+qtext :: Char -> Bool+qtext c = asciiqtext (ord c) || utf8NonAscii c+  where+    asciiqtext n =+      n == 33+        || (n >= 35 && n <= 91)+        || (n >= 93 && n <= 126)++-- | Obsolete @qtext@.+--+-- > obs-qtext = obs-NO-WS-CTL+--+-- @since 0.1.0.0+qtextObs :: Char -> Bool+qtextObs = obsNoWsCtl++-- | Characters that can follow a backslash.+--+-- > quoted-pair     =   ("\" (VCHAR / WSP)) / obs-qp+--+-- @since 0.1.0.0+quotedPair :: Char -> Bool+quotedPair c = vchar c || wsp c++-- | Obsolete characters that can be escaped with a backslash.+--+-- > obs-qp          =   "\" (%d0 / obs-NO-WS-CTL / LF / CR)+--+-- @since 0.1.0.0+quotedPairObs :: Char -> Bool+quotedPairObs c =+  obsNoWsCtl c+    || c == '\r'+    || c == '\n'+    || c == '\0'
+ src/Addy/Internal/Parser.hs view
@@ -0,0 +1,543 @@+-- |+--+-- 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 parsing functions.+module Addy.Internal.Parser+  ( Mode (..),+    Atom (..),+    parse,+    parseWithMode,+    nameAddr,+    addrSpec,+    localPartP,+    domainP,+    displayNameP,+    word,+    atom,+    dotAtom,+    dotAtomLh,+    dotAtomRh,+    quoted,+    quotedLh,+    cfws,+  )+where++import Addy.Internal.Char+import Addy.Internal.Types+import Addy.Internal.Validation+import Data.Attoparsec.Text ((<?>))+import qualified Data.Attoparsec.Text as Atto+import Data.Foldable+import qualified Data.Text as Text+import qualified Net.IP as IP+import qualified Net.IPv4 as IP4+import qualified Net.IPv6 as IP6+import qualified Validation++-- | Parsing mode.+--+-- @since 0.1.0.0+data Mode+  = -- | Only support non-obsoleted addresses.+    Strict+  | -- | Include support for obsolete addresses.+    Lenient+  deriving (Eq, Show)++-- | RFC 5322 @atom@.+--+-- @since 0.1.0.0+data Atom = Atom (Maybe CommentContent) Text (Maybe CommentContent)+  deriving (Show)++instance Semigroup Atom where+  (<>) (Atom x0 y0 z0) (Atom x1 y1 z1) =+    Atom (x0 <> x1) (y0 <> y1) (z0 <> z1)++instance Monoid Atom where+  mempty = Atom Nothing mempty Nothing++-- | FIXME: Write description for atomJoin+--+-- @since 0.1.0.0+atomJoin :: Foldable t => Char -> t Atom -> Atom+atomJoin sep as+  | null as = mempty+  | otherwise = foldr1 go as+  where+    go :: Atom -> Atom -> Atom+    go (Atom c0 t0 c1) (Atom c2 t1 c3) =+      Atom (go' c0 c2) (t0 <> one sep <> t1) (go' c1 c3)+    go' :: Maybe CommentContent -> Maybe CommentContent -> Maybe CommentContent+    go' (Just x) (Just y) = Just (x <> CC (one ' ') <> y)+    go' x y = x <|> y++-- | An email address parser.+--+-- @since 0.1.0.0+parse :: Mode -> Atto.Parser EmailAddr+parse m = cleanComments <$> (nameAddr m <|> addrSpec m)+  where+    -- Since white space is allowed all over the place, filter out+    -- comments that are just white space.+    cleanComments :: EmailAddr -> EmailAddr+    cleanComments addr@EmailAddr {_comments} =+      addr+        { _comments =+            filter+              ( \(Comment _ (CC t)) -> not $ Text.null (Text.strip t)+              )+              _comments+        }++-- | Run the parser and then validate the resulting address.+--+-- @since 0.1.0.0+parseWithMode :: Mode -> Text -> Either (NonEmpty Error) EmailAddr+parseWithMode mode text = do+  addr <-+    first (toText >>> ParserFailedError >>> one) $+      Atto.parseOnly+        ( parse mode <* (Atto.endOfInput <?> "unparsed input")+        )+        text+  case validateEmailAddr addr of+    Validation.Success ea -> Right ea+    Validation.Failure es -> Left es++-- | Parse email addresses in the @name-addr@ format.+--+-- @since 0.1.0.0+nameAddr :: Mode -> Atto.Parser EmailAddr+nameAddr mode = do+  dp <- optional (displayNameP mode)+  c0 <- optional (cfws mode)+  _ <- Atto.char '<'+  (c1, lp) <- localPartP mode <* Atto.char '@'+  (dn, c2) <- domainP mode+  _ <- Atto.char '>'+  c3 <- optional (cfws mode)+  let (dpc0, dpt, dpc1) = case dp of+        Nothing -> (Nothing, Nothing, Nothing)+        Just (Atom x y z) -> (x, Just (DP y), z)+  pure $+    EmailAddr+      { _displayName = dpt,+        _localPart = lp,+        _domain = dn,+        _comments =+          catMaybes+            [ Comment BeforeDisplayName <$> dpc0,+              Comment AfterDisplayName <$> dpc1,+              Comment AfterDisplayName <$> c0,+              Comment BeforeLocalPart <$> c1,+              Comment AfterDomain <$> c2,+              Comment AfterAddress <$> c3+            ]+      }++-- | Parse email addresses in the @addr-spec@ format.+--+-- @since 0.1.0.0+addrSpec :: Mode -> Atto.Parser EmailAddr+addrSpec mode = do+  (c0, lp) <- localPartP mode <* Atto.char '@'+  (dn, c1) <- domainP mode+  pure $+    EmailAddr+      { _displayName = Nothing,+        _localPart = lp,+        _domain = dn,+        _comments =+          catMaybes+            [ Comment BeforeLocalPart <$> c0,+              Comment AfterDomain <$> c1+            ]+      }++-- | Parse the @local-part@ of an email address.+--+-- RFC 5322 §3.4.1+--+-- > local-part = dot-atom / quoted-string / obs-local-part+--+-- @since 0.1.0.0+localPartP :: Mode -> Atto.Parser (Maybe CommentContent, LocalPart)+localPartP mode = go <?> "local part"+  where+    go =+      case mode of+        Strict -> do+          Atom c0 t c1 <- (dotAtomLh <* thenAt) <|> (quotedLh <* thenAt)+          pure (c0 <> c1, LP t)+        Lenient -> do+          -- Obsolete comes before quoted since the obsolete syntax allows+          -- multiple quoted strings separated by dots.+          Atom c0 t c1 <-+            Atto.choice+              [ dotAtom mode <* thenAt,+                obsLocalPart <* thenAt,+                quoted mode <* thenAt+              ]+          pure (c0 <> c1, LP t)+    -- > obs-local-part = word *("." word)+    obsLocalPart :: Atto.Parser Atom+    obsLocalPart = do+      t0 <- word mode+      ts <- many (Atto.char '.' *> word mode)+      pure (atomJoin '.' (t0 : ts))+    thenAt :: Atto.Parser ()+    thenAt =+      Atto.peekChar'+        >>= bool empty pass . (== '@')++-- | Domain name parser.+--+-- @since 0.1.0.0+domainP :: Mode -> Atto.Parser (Domain, Maybe CommentContent)+domainP mode = go <?> "domain name"+  where+    go =+      case mode of+        Strict ->+          domainNameP+            <|> domainLiteralP (fws mode $> CC (one ' '))+        Lenient ->+          obsDomainP+            <|> domainNameP+            <|> domainLiteralP (cfws mode)+    domainNameP :: Atto.Parser (Domain, Maybe CommentContent)+    domainNameP = do+      Atom c0 t c1 <- dotAtomRh+      pure (Domain $ DN t, c0 <> c1)+    -- > domain-literal  =   [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS]+    domainLiteralP ::+      Atto.Parser CommentContent ->+      Atto.Parser (Domain, Maybe CommentContent)+    domainLiteralP lh = do+      c0 <- optional lh+      t <- addressLiteral mode+      c1 <- optional (cfws mode)+      pure (DomainLiteral t, c0 <> c1)+    -- > obs-domain = atom *("." atom)+    obsDomainP :: Atto.Parser (Domain, Maybe CommentContent)+    obsDomainP = do+      t0 <- atom mode+      ts <- many (Atto.char '.' *> atom mode)+      let Atom c0 t c1 = atomJoin '.' (t0 : ts)+      pure (Domain $ DN t, c0 <> c1)++-- | Parse a display name.+--+-- @since 0.1.0.0+displayNameP :: Mode -> Atto.Parser Atom+displayNameP mode =+  case mode of+    Strict -> phrase+    Lenient -> phrase <|> obsPhrase+  where+    phrase = (<?> "display name") $ do+      -- Always strict since in lenient mode we'll fall back to+      -- obsolete mode anyways.+      w0 <- word Strict+      ws <- many (word Strict)+      pure (atomJoin ' ' (w0 : ws))+    obsPhrase = (<?> "obsolete display name") $ do+      w0 <- word mode+      ws <-+        many+          ( word mode+              <|> Atom Nothing <$> (Atto.char '.' <&> one) <*> pure Nothing+              <|> Atom <$> (cfws mode <&> Just) <*> pure mempty <*> pure Nothing+          )+      pure (atomJoin ' ' (w0 : ws))++-- | An atom or quoted string.+--+-- > word = atom / quoted-string+--+-- @since 0.1.0.0+word :: Mode -> Atto.Parser Atom+word mode = atom mode <|> quoted mode++-- | Parse an unquoted atom.+--+-- > atom            =   [CFWS] 1*atext [CFWS]+--+-- @since 0.1.0.0+atom :: Mode -> Atto.Parser Atom+atom mode =+  Atom+    <$> optional (cfws mode)+    <*> atextP+    <*> optional (cfws mode)++-- | Parse an unquoted atom that is allowed to contain periods.+--+-- @since 0.1.0.0+dotAtom' ::+  Atto.Parser CommentContent ->+  Atto.Parser CommentContent ->+  Atto.Parser Atom+dotAtom' lh rh = do+  c0 <- optional lh+  t0 <- atextP+  ts <- many (Atto.char '.' *> atextP)+  c1 <- optional rh+  pure (Atom c0 (Text.intercalate "." (t0 : ts)) c1)++-- | RFC 5322 @dot-atom@.+--+-- @since 0.1.0.0+dotAtom :: Mode -> Atto.Parser Atom+dotAtom mode = dotAtom' (cfws mode) (cfws mode)++-- | Strict @dot-atom-lh@ from RFC 5322 errata.+--+-- > dot-atom-lh = [CFWS] dot-atom-text [FWS]+--+-- @since 0.1.0.0+dotAtomLh :: Atto.Parser Atom+dotAtomLh =+  dotAtom'+    (cfws Strict)+    (CC <$> (fws Strict $> one ' '))++-- | Strict @dot-atom-rh@ from RFC 5322 errata.+--+-- > dot-atom-rh = [FWS] dot-atom-text [CFWS]+--+-- @since 0.1.0.0+dotAtomRh :: Atto.Parser Atom+dotAtomRh =+  dotAtom'+    (CC <$> (fws Strict $> one ' '))+    (cfws Strict)++-- | Is a character allowed in an atom?+--+-- RFC 5322 §3.2.3+--+-- @since 0.1.0.0+atextP :: Atto.Parser Text+atextP = Atto.takeWhile1 atext++-- | A quoted string.+--+-- RFC5322 §3.2.4+--+-- > qtext           =   %d33 /             ; Printable US-ASCII+-- >                     %d35-91 /          ;  characters not including+-- >                     %d93-126 /         ;  "\" or the quote character+-- >                     obs-qtext+-- >+-- > qcontent        =   qtext / quoted-pair+-- >+-- > quoted-string   =   [CFWS]+-- >                     DQUOTE *([FWS] qcontent) [FWS] DQUOTE+-- >                     [CFWS]+-- >+-- > obs-qtext       =   obs-NO-WS-CTL+-- >+--+-- RFC 6532 §3.2+--+-- > qtext   =/  UTF8-non-ascii+--+-- RFC 5322 errata item 3135 <https://www.rfc-editor.org/errata/eid3135>+--+--+-- > quoted-string   =   [CFWS]+-- >                     DQUOTE ((1*([FWS] qcontent) [FWS]) / FWS) DQUOTE+-- >                     [CFWS]+--+-- This is the rule we use since it's consistent with the text of the RFC.+--+-- @since 0.1.0.0+quoted :: Mode -> Atto.Parser Atom+quoted mode = quoted' (cfws mode) (cfws mode) mode++-- | General-purpose quoted-string parser.+--+-- @since 0.1.0.0+quoted' ::+  Atto.Parser CommentContent ->+  Atto.Parser CommentContent ->+  Mode ->+  Atto.Parser Atom+quoted' lh rh mode = (<?> "quoted content") $ do+  c0 <- optional lh+  _ <- Atto.char '"'+  t <- Atto.many1 ((<>) <$> fws' <*> qcontent)+  w <- fws'+  _ <- Atto.char '"'+  c1 <- optional rh+  pure (Atom c0 (mconcat t <> w) c1)+  where+    -- Characters that are allowed in the quotes:+    qcontent :: Atto.Parser Text+    qcontent = qtextP <|> quotedPairP mode+    qtextP :: Atto.Parser Text+    qtextP = case mode of+      Strict -> Atto.takeWhile1 qtext+      Lenient ->+        Atto.takeWhile1 (\c -> qtext c || qtextObs c)+          <&> Text.filter (not . qtextObs)+    fws' = (fws mode $> one ' ') <|> pure mempty++-- | Strict @quoted-string-lh@ from RFC 5322 errata.+--+-- @since 0.1.0.0+quotedLh :: Atto.Parser Atom+quotedLh =+  quoted'+    (cfws Strict)+    (CC <$> (fws Strict $> one ' '))+    Strict++-- | Parse backslash escapes:+--+-- @since 0.1.0.0+quotedPairP :: Mode -> Atto.Parser Text+quotedPairP mode = go <?> "quoted char"+  where+    go = Atto.char '\\' *> allowed+    allowed = case mode of+      Strict ->+        Atto.satisfy quotedPair+          <&> one+      Lenient ->+        Atto.satisfy (\c -> quotedPair c || quotedPairObs c)+          <&> (one >>> Text.filter (not . quotedPairObs))++-- | Comments and folding white space.+--+-- > ctext           =   %d33-39 /          ; Printable US-ASCII+-- >                     %d42-91 /          ;  characters not including+-- >                     %d93-126 /         ;  "(", ")", or "\"+-- >                     obs-ctext+-- >+-- > obs-ctext       =   obs-NO-WS-CTL+-- >+-- > ccontent        =   ctext / quoted-pair / comment+-- >+-- > comment         =   "(" *([FWS] ccontent) [FWS] ")"+-- >+-- > CFWS            =   (1*([FWS] comment) [FWS]) / FWS+--+-- @since 0.1.0.0+cfws :: Mode -> Atto.Parser CommentContent+cfws mode =+  (<?> "comment or space")+    (cfws' <|> (CC <$> fws mode))+  where+    cfws' :: Atto.Parser CommentContent+    cfws' = do+      cs <- Atto.many1 (fws' *> comment) <* fws'+      pure (CC $ mconcat cs)+    comment :: Atto.Parser Text+    comment = do+      _ <- Atto.char '('+      ts <- many (fws' *> (mconcat <$> Atto.many1 ccontent)) <* fws'+      _ <- Atto.char ')'+      pure (Text.intercalate " " ts)+    ccontent :: Atto.Parser Text+    ccontent = ctextP <|> quotedPairP mode <|> comment+    ctextP :: Atto.Parser Text+    ctextP = case mode of+      Strict ->+        Atto.takeWhile1 ctext+      Lenient ->+        Atto.takeWhile1 (\c -> ctext c || ctextObs c)+          <&> Text.filter (not . ctextObs)+    fws' :: Atto.Parser ()+    fws' = void (optional (fws mode))++-- | Folding white space.+--+-- > FWS             =   ([*WSP CRLF] 1*WSP) /  obs-FWS+-- >                                        ; Folding white space+-- >+-- > obs-FWS         =   1*WSP *(CRLF 1*WSP)+--+-- @since 0.1.0.0+fws :: Mode -> Atto.Parser Text+fws = \case+  Strict -> do+    w0 <- (Atto.takeWhile wsp <* crlf) <|> pure Text.empty+    w1 <- Atto.takeWhile1 wsp+    pure (w0 <> w1)+  Lenient -> do+    w0 <- Atto.takeWhile1 wsp+    ws <- many (crlf *> Atto.takeWhile1 wsp)+    pure (w0 <> mconcat ws)+  where+    crlf = Atto.string "\r\n"++-- | Parse a domain/address literal.+--+-- > dtext           =   %d33-90 /          ; Printable US-ASCII+-- >                     %d94-126 /         ;  characters not including+-- >                     obs-dtext          ;  "[", "]", or "\"+-- > obs-dtext       =   obs-NO-WS-CTL / quoted-pair+--+-- @since 0.1.0.0+addressLiteral :: Mode -> Atto.Parser AddressLiteral+addressLiteral mode =+  (<?> "address literal")+    $ Atto.choice+    $ map+      wrap+      [ IpAddressLiteral . IP.fromIPv6 <$> (Atto.string "IPv6:" *> IP6.parser),+        TaggedAddressLiteral <$> tag <*> (Atto.char ':' *> lit),+        IpAddressLiteral . IP.fromIPv4 <$> IP4.parser,+        AddressLiteral <$> lit+      ]+  where+    wrap :: Atto.Parser a -> Atto.Parser a+    wrap p =+      Atto.char '['+        *> p+        <* optional (fws mode)+        <* Atto.char ']'+    tag :: Atto.Parser AddressTag+    tag = Atto.takeWhile1 (\c -> c /= ':' && dtext c) <&> AT+    lit :: Atto.Parser Literal+    lit =+      Lit . mconcat+        <$> many+          ( do+              f0 <- fws'+              ts <- dtextP+              f1 <- fws'+              pure (f0 <> ts <> f1)+          )+    dtextP :: Atto.Parser Text+    dtextP =+      case mode of+        Strict ->+          Atto.takeWhile1 dtext+        Lenient ->+          -- Allow obsolete syntax, but don't capture it.+          mconcat+            <$> Atto.many1+              ( ( Atto.takeWhile1 (\c -> dtext c || obsNoWsCtl c)+                    <&> Text.filter (not . obsNoWsCtl)+                )+                  <|> (quotedPairP mode $> one '-')+              )+    fws' :: Atto.Parser Text+    fws' = fws mode $> one ' ' <|> pure mempty
+ src/Addy/Internal/Render.hs view
@@ -0,0 +1,187 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}++-- |+--+-- 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 functions to render an 'EmailAddr' to a 'TB.Builder'.+module Addy.Internal.Render+  ( Mode (..),+    render,+    renderToText,+    renderAddrSpec,+    renderDisplayName,+    renderComments,+  )+where++import Addy.Internal.Char+import Addy.Internal.Types+import qualified Data.Text as Text+import qualified Data.Text.Lazy.Builder as TB+import qualified Net.IP as IP+import Text.Show (Show (..), showParen, showString)+import Prelude hiding (show)++-- | Render mode.+--+-- @since 0.1.0.0+data Mode+  = -- | Render the entire email address, including the optional+    -- display name and comments.+    Full+  | -- | Only render the simplest form of the email address.  Only the+    -- 'LocalPart' and 'Domain' are rendered in this mode.+    Short++-- | Render an email address.+--+-- @since 0.1.0.0+render :: Mode -> EmailAddr -> TB.Builder+render = \case+  Short ->+    renderAddrSpec Short+  Full -> \addr@EmailAddr {..} ->+    case _displayName of+      Nothing ->+        renderAddrSpec Full addr+          <> renderComments Full AfterAddress _comments+      Just name ->+        mconcat+          [ renderComments Full BeforeDisplayName _comments,+            renderDisplayName name <> TB.singleton ' ',+            renderComments Full AfterDisplayName _comments,+            TB.singleton '<' <> renderAddrSpec Full addr <> TB.singleton '>',+            renderComments Full AfterAddress _comments+          ]++-- | Render an email address in @addr-spec@ format.+--+-- @since 0.1.0.0+renderAddrSpec :: Mode -> EmailAddr -> TB.Builder+renderAddrSpec mode EmailAddr {..} =+  mconcat+    [ renderComments mode BeforeLocalPart _comments,+      lp _localPart <> TB.singleton '@' <> dn _domain,+      renderComments mode AfterDomain _comments+    ]+  where+    lp :: LocalPart -> TB.Builder+    lp (LP t)+      | mustQuoteLocalPart t = wrap '"' '"' t+      | otherwise = TB.fromText t+    dn :: Domain -> TB.Builder+    dn = \case+      Domain (DN t) ->+        TB.fromText t+      DomainLiteral lit ->+        wrap '[' ']' $ case lit of+          IpAddressLiteral ip ->+            if IP.isIPv6 ip+              then "IPv6:" <> IP.encode ip+              else IP.encode ip+          TaggedAddressLiteral (AT tag) (Lit body) ->+            tag <> ":" <> body+          AddressLiteral (Lit body) ->+            body++-- | Render a display name.+--+-- @since 0.1.0.0+renderDisplayName :: DisplayName -> TB.Builder+renderDisplayName (DP t)+  | Text.all (\c -> atext c || wsp c) t =+    TB.fromText t+  | otherwise =+    wrap '"' '"' t++-- | Render comments that have the given 'CommentLoc'.  The comment+-- location is also used to decide where to introduce white space.+--+-- @since 0.1.0.0+renderComments :: Mode -> CommentLoc -> [Comment] -> TB.Builder+renderComments Short _ _ = mempty+renderComments Full loc cs =+  case go (== loc) cs of+    Nothing -> mempty+    Just tb -> case loc of+      BeforeDisplayName -> TB.singleton ' ' <> tb+      AfterDisplayName -> tb <> TB.singleton ' '+      BeforeLocalPart -> tb <> TB.singleton ' '+      AfterDomain -> TB.singleton ' ' <> tb+      AfterAddress -> TB.singleton ' ' <> tb+  where+    go :: (CommentLoc -> Bool) -> [Comment] -> Maybe TB.Builder+    go f cs =+      filter (\(Comment loc (CC t)) -> f loc && not (Text.null t)) cs+        & map (\(Comment _ (CC t)) -> t)+        & Text.intercalate " "+        & \t ->+          if Text.null t+            then Nothing+            else Just $ wrap '(' ')' t++-- | Render the given address as text.+--+-- @since 0.1.0.0+renderToText :: Mode -> EmailAddr -> Text+renderToText m =+  render m+    >>> TB.toLazyText+    >>> toStrict++-- | Wrap and quote some text.+--+-- @since 0.1.0.0+wrap :: Char -> Char -> Text -> TB.Builder+wrap lh rh t =+  mconcat+    [ TB.singleton lh,+      Text.foldl' escape mempty t,+      TB.singleton rh+    ]+  where+    escape :: TB.Builder -> Char -> TB.Builder+    escape tb c+      | c == lh || c == rh || c == '\\' =+        tb <> TB.singleton '\\' <> TB.singleton c+      | otherwise =+        tb <> TB.singleton c++-- | 'True' if the give text, when used as the @local-part@ of an+-- email address must be wrapped in quotation marks.+--+-- @since 0.1.0.0+mustQuoteLocalPart :: Text -> Bool+mustQuoteLocalPart name =+  Text.any+    ( \c ->+        c == '"'+          || c == '\\'+          || c == ')'+          || c == '('+          || c == '@'+          || wsp c+    )+    name+    || Text.isPrefixOf "." name+    || Text.isSuffixOf "." name+    || Text.isInfixOf ".." name+    || Text.null name -- Yes, this is totally legit.++-- Orphan instance that renders the email address.+instance Show EmailAddr where+  showsPrec d addr =+    showParen (d > 10) $+      showString "EmailAddr " . showsPrec d (render Full addr)
+ src/Addy/Internal/Types.hs view
@@ -0,0 +1,370 @@+-- |+--+-- 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
+ src/Addy/Internal/Validation.hs view
@@ -0,0 +1,238 @@+-- |+--+-- 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)
+ test/Main.hs view
@@ -0,0 +1,32 @@+-- |+--+-- 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+module Main+  ( main,+  )+where++import qualified ParserTest+import qualified RenderTest+import Test.Tasty++-- | Main.+main :: IO ()+main =+  defaultMain $+    testGroup+      "Email"+      [ ParserTest.test,+        RenderTest.test+      ]
+ test/ParserTest.hs view
@@ -0,0 +1,129 @@+-- |+--+-- 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+module ParserTest+  ( test,+  )+where++import Addy.Internal.Parser as P+import Addy.Internal.Render ()+import Addy.Internal.Types+import qualified Hedgehog+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit+import Test.Tasty.Hedgehog+import TestData++test :: TestTree+test =+  testGroup+    "Parser"+    [ testCase "isemail tests" testParserWithIsEmail,+      testCase "RFC 5322 examples" testRfc5322Examples,+      testCase "Wikipedia internationalization examples" testWikipediaIntExamples,+      testCase "Miscellaneous examples" testMiscExamples,+      testProperty+        "Generated short addresses"+        (testGeneratedExamples genShortEmail),+      testProperty+        "Generated long addresses"+        (testGeneratedExamples genLongEmail)+    ]++testRfc5322Examples :: Assertion+testRfc5322Examples = do+  mapM_ (fst >>> go) rfc5322Examples+  -- Only works in lenient mode:+  mapM_ (\(a, _, _) -> assertParse Lenient a) rfc5322ObsExamples+  where+    go :: Text -> Assertion+    go t = do+      assertParse Strict t+      assertParse Lenient t++testWikipediaIntExamples :: Assertion+testWikipediaIntExamples =+  mapM_ go wikipediaIntExamples+  where+    go t = do+      assertParse Strict t+      assertParse Lenient t++testMiscExamples :: Assertion+testMiscExamples =+  forM_ examples $ \t -> do+    assertParse Strict t+    assertParse Lenient t+  where+    examples =+      miscExamples+        <> [ "(((((((((())))))))))user@example.org"+           ]++testGeneratedExamples :: Hedgehog.Gen Text -> Hedgehog.Property+testGeneratedExamples gen =+  Hedgehog.property $ do+    source <- Hedgehog.forAll gen+    let result = runParse source+    Hedgehog.annotateShow result+    Hedgehog.assert (isRight result)+  where+    runParse = parseWithMode Strict++assertParse :: Mode -> Text -> Assertion+assertParse mode text =+  let r = parseWithMode mode text+   in assertBool+        (toString text <> " " <> show r <> " " <> show mode)+        (isRight r)++testParserWithIsEmail :: Assertion+testParserWithIsEmail = mapM_ go =<< isEmailTests+  where+    go :: (IsEmailTest, IsEmailCat) -> Assertion+    go (isemail, cat) = case cat of+      CatError -> do+        shouldFail Strict isemail+        shouldFail Lenient isemail+      CatDeprec -> do+        shouldFail Strict isemail+        shouldPass Lenient isemail+      CatOkay ->+        shouldPass Strict isemail+    shouldPass, shouldFail :: Mode -> IsEmailTest -> Assertion+    shouldPass = expect isRight+    shouldFail = expect isLeft+    expect ::+      (Either (NonEmpty Error) EmailAddr -> Bool) ->+      Mode ->+      IsEmailTest ->+      Assertion+    expect f mode ie =+      let result = runParse mode ie+       in assertBool (report mode ie result) (f result)+    runParse :: Mode -> IsEmailTest -> Either (NonEmpty Error) EmailAddr+    runParse mode = parseWithMode mode . ietAddr+    report :: Mode -> IsEmailTest -> Either (NonEmpty Error) EmailAddr -> String+    report mode isemail result =+      mconcat+        [ "IsEmail test id ",+          show (ietId isemail),+          " in mode: ",+          show mode,+          " did not expect: ",+          show result,+          " from: ",+          toString (ietAddr isemail)+        ]
+ test/README.md view
@@ -0,0 +1,6 @@+Test Data+=========++Test data was taken from the [isemail][] project.++[isemail]: https://github.com/dominicsayers/isemail
+ test/RenderTest.hs view
@@ -0,0 +1,156 @@+-- |+--+-- 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+module RenderTest+  ( test,+  )+where++import qualified Addy.Internal.Parser as Parser+import Addy.Internal.Render+import Addy.Internal.Types+import Data.Char+import qualified Data.Text as Text+import qualified Data.Text.Lazy.Builder as TB+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit+import TestData++test :: TestTree+test =+  testGroup+    "Render"+    [ testCase "isemail tests" testFromisEmailTests,+      testCase "RFC 5322 examples" testRfcExamples,+      testCase "Obsolete examples" testObsExamples,+      testCase "Oddities" testOddities,+      testCase "Wikipedia" testWikipediaExamples,+      testCase "Misc" testMiscExamples+    ]++testRfcExamples :: Assertion+testRfcExamples =+  mapM_ go rfc5322Examples+  where+    go :: (Text, Text) -> Assertion+    go (source, expectS) = do+      (_, actualS, actualF) <- roundTrip Parser.Strict source+      let expectF =+            -- We don't render the brackets unless they are needed.+            if Text.isPrefixOf "<" source+              && Text.isSuffixOf ">" source+              then Text.drop 1 (Text.dropEnd 1 source)+              else source+      actualS @?= expectS+      actualF @?= expectF+      idempotentTest actualS actualF++testObsExamples :: Assertion+testObsExamples =+  mapM_ go rfc5322ObsExamples+  where+    go :: (Text, Text, Text) -> Assertion+    go (source, simple, full) = do+      (_, actualS, actualF) <- roundTrip Parser.Lenient source+      actualS @?= simple+      actualF @?= full+      idempotentTest actualS actualF++-- | Weird stuff that we'll parse but won't render.+testOddities :: Assertion+testOddities =+  mapM_+    go+    [ ( "(some (ne(st)ed) comment) example@example.com",+        "(some nested comment) example@example.com",+        "example@example.com"+      )+    ]+  where+    go :: (Text, Text, Text) -> Assertion+    go (source, full, simple) = do+      (_, actualS, actualF) <- roundTrip Parser.Strict source+      actualS @?= simple+      actualF @?= full++testMiscExamples :: Assertion+testMiscExamples =+  forM_ miscExamples $ \source -> do+    (_, actualS, actualF) <- roundTrip Parser.Strict source+    actualS @?= source+    actualF @?= source++testFromisEmailTests :: Assertion+testFromisEmailTests = do+  tests <- isEmailTests+  mapM_+    go+    ( filter+        ( \(iet, cat) ->+            cat == CatOkay+              && ietId iet /= 42 -- Unnecessary quoting.+              && ietId iet /= 45 -- Unnecessary quoting.+              && ietId iet /= 55 -- Unnecessary quoting.+              && ietId iet /= 60 -- Unnecessary quoting.+              && ietId iet /= 79 -- We format better.+              && ietId iet /= 81 -- We format better.+              && ietId iet /= 85 -- We format better.+              && ietId iet /= 88 -- We strip \r\n.+              && ietId iet /= 92 -- We flattten comments.+              && ietId iet /= 144 -- We strip \r\n.+              && ietId iet /= 148 -- We strip \r\n.+              && ietId iet /= 153 -- We strip \r\n.+        )+        tests+    )+  where+    go :: (IsEmailTest, IsEmailCat) -> Assertion+    go (iet, _) = do+      (_, _, actualF) <- roundTrip Parser.Strict (ietAddr iet)+      fixup actualF @?= fixup (ietAddr iet)+    -- Ignore white space changes and revert IPv6 address formatting+    fixup :: Text -> Text+    fixup = Text.filter (not . isSpace) . Text.replace ":0:" "::"++testWikipediaExamples :: Assertion+testWikipediaExamples =+  mapM_ go wikipediaIntExamples+  where+    go :: Text -> Assertion+    go source = do+      (_, simple, full) <- roundTrip Parser.Strict source+      simple @?= source+      full @?= source++roundTrip :: MonadFail m => Parser.Mode -> Text -> m (EmailAddr, Text, Text)+roundTrip pmode t =+  case Parser.parseWithMode pmode t of+    Left e -> fail (show t <> " " <> show e)+    Right addr ->+      pure+        ( addr,+          toStrict $ TB.toLazyText (render Short addr),+          toStrict $ TB.toLazyText (render Full addr)+        )++-- | Test that parsing the rendered output and then rendering it again+-- produces the same results.+idempotentTest :: Text -> Text -> Assertion+idempotentTest simple full = do+  (_, x, y) <- roundTrip Parser.Strict simple+  x @?= simple -- Parsing and rending a simple address should+  y @?= simple -- produce a simple rendering both times.+  (_, z, w) <- roundTrip Parser.Strict full+  z @?= simple+  w @?= full
+ test/TestData.hs view
@@ -0,0 +1,235 @@+-- |+--+-- 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+module TestData+  ( IsEmailTest (..),+    IsEmailCat (..),+    isEmailTests,+    rfc5322Examples,+    rfc5322ObsExamples,+    wikipediaIntExamples,+    miscExamples,+    genShortEmail,+    genLongEmail,+  )+where++import Addy.Internal.Char (utf8NonAscii)+import Data.Aeson (FromJSON)+import qualified Data.Aeson as Aeson+import Data.Char+import qualified Data.Text as Text+import Hedgehog+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range+import Network.HTTP.Types.URI (urlDecode)+import Test.Tasty.HUnit++data IsEmailTest = IET+  { ietId :: Int,+    ietAddr :: Text,+    ietCat :: Text,+    ietDia :: Text+  }+  deriving stock (Generic, Show)+  deriving anyclass (FromJSON)++data IsEmailCat+  = CatError+  | CatDeprec+  | CatOkay+  deriving (Eq, Show)++isEmailTests :: IO [(IsEmailTest, IsEmailCat)]+isEmailTests = do+  let fileName = "test/isemail.json"+  tests <- (Aeson.decode <$> readFileLBS fileName) >>= \case+    Nothing -> assertFailure "failed to load the isemail.json file"+    Just ts -> pure ts+  pure $ map (\t -> (decodeAddr t, decodeCat t)) tests++-- | The XSL file URL-encodes the email addresses.  Additionally, the+-- XML file encodes some characters by adding 0x2400 to them.+decodeAddr :: IsEmailTest -> IsEmailTest+decodeAddr iet@IET {ietAddr} =+  iet+    { ietAddr = unCtrl (decode ietAddr)+    }+  where+    decode :: Text -> Text+    decode = decodeUtf8 . urlDecode False . encodeUtf8+    unCtrl :: Text -> Text+    unCtrl =+      let f c =+            if c >= '\x2400' && c <= '\x241f'+              then chr (ord c - 0x2400)+              else c+       in Text.map f++decodeCat :: IsEmailTest -> IsEmailCat+decodeCat iet = go & override+  where+    go = case ietCat iet of+      "ISEMAIL_ERR" -> CatError+      "ISEMAIL_DEPREC" -> CatDeprec+      _ -> CatOkay+    override cat+      -- We enforce character limits so these will fail.+      | ietId iet == 26 = CatError+      | ietId iet == 28 = CatError+      | ietId iet == 41 = CatError+      -- This is obviously wrong if you put even the slightest amount+      -- of thought into it.+      | ietId iet == 43 = CatError+      -- This one is a bit silly.  Even if that form is deprecated+      -- it would then pass under the general @domain-literal@ rule+      -- in RFC 5322 §3.4.1.+      | ietId iet == 71 = CatOkay+      -- RFC 5322 (and its errata) clearly allows whitespace before+      -- and after the at sign.+      | ietId iet == 85 = CatOkay+      -- It's not worth supporting the very obsolete syntax of having+      -- multiple empty lines before an email address.+      | ietId iet == 89 = CatError+      | ietId iet == 149 = CatError+      -- These should be marked at deprecated.+      | ietId iet == 115 = CatDeprec+      | ietId iet == 116 = CatDeprec+      | ietId iet == 117 = CatDeprec+      -- Obsolete and won't pass validation because they only contain+      -- obsolete characters!+      | ietId iet == 124 = CatError+      | ietId iet == 125 = CatError+      | ietId iet == 134 = CatError+      | ietId iet == 138 = CatError+      | ietId iet == 139 = CatError+      -- Nothing to override.+      | otherwise = cat++rfc5322Examples :: [(Text, Text)]+rfc5322Examples =+  [ ("John Doe <jdoe@machine.example>", "jdoe@machine.example"),+    ("Mary Smith <mary@example.net>", "mary@example.net"),+    ("Michael Jones <mjones@machine.example>", "mjones@machine.example"),+    ("\"Joe Q. Public\" <john.q.public@example.com>", "john.q.public@example.com"),+    ("Mary Smith <mary@x.test>", "mary@x.test"),+    ("jdoe@example.org", "jdoe@example.org"),+    ("Who? <one@y.test>", "one@y.test"),+    ("<boss@nil.test>", "boss@nil.test"),+    ("\"Giant; \\\"Big\\\" Box\" <sysservices@example.net>", "sysservices@example.net"),+    ("Pete <pete@silly.example>", "pete@silly.example"),+    ("Ed Jones <c@a.test>", "c@a.test"),+    ("joe@where.test", "joe@where.test"),+    ("John <jdoe@one.test>", "jdoe@one.test"),+    ("\"Mary Smith: Personal Account\" <smith@home.example>", "smith@home.example"),+    ("Jane Brown <j-brown@other.example>", "j-brown@other.example"),+    ("John <jdoe@one.test> (my dear friend)", "jdoe@one.test")+  ]++rfc5322ObsExamples :: [(Text, Text, Text)]+rfc5322ObsExamples =+  [ ( "Pete(A nice \\) chap) <pete(his account)@silly.test(his host)>",+      "pete@silly.test",+      "Pete (A nice \\) chap) <(his account) pete@silly.test (his host)>"+    ),+    ( "Chris Jones <c@(Chris's host.)public.example>",+      "c@public.example",+      "Chris Jones <c@public.example (Chris's host.)>"+    )+  ]++wikipediaIntExamples :: [Text]+wikipediaIntExamples =+  [ "Pelé@example.com",+    "δοκιμή@παράδειγμα.δοκιμή",+    "我買@屋企.香港",+    "二ノ宮@黒川.日本",+    "медведь@с-балалайкой.рф",+    "संपर्क@डाटामेल.भारत"+  ]++-- | Other examples that should pass.+--+-- > (source, simple-format)+miscExamples :: [Text]+miscExamples =+  [ "example+label@example.com",+    "a@b",+    "simple@example.com",+    "very.common@example.com",+    "disposable.style.email.with+symbol@example.com",+    "other.email-with-hyphen@example.com",+    "fully-qualified-domain@example.com",+    "user.name+tag+sorting@example.com",+    "x@example.com",+    "example-indeed@strange-example.com",+    "admin@mailserver1",+    "example@s.example",+    "\"john..doe\"@example.org",+    "mailhost!username@example.org",+    "user%example.com@example.org"+  ]++genLocalPart :: Gen Text+genLocalPart =+  Gen.filter okay (Gen.text (Range.linear 1 30) unicode)+  where+    okay :: Text -> Bool+    okay t =+      Text.all allowedChar t+        && not (Text.isPrefixOf "." t)+        && not (Text.isSuffixOf "." t)++genDomain :: Gen Text+genDomain =+  Gen.filter okay (Gen.text (Range.linear 1 50) unicode)+  where+    okay :: Text -> Bool+    okay t =+      Text.all allowedChar t+        && not (Text.isPrefixOf "-" t)+        && not (Text.isSuffixOf "-" t)++genShortEmail :: Gen Text+genShortEmail = do+  localPart <- genLocalPart+  domain <- genDomain+  pure (localPart <> "@" <> domain)++genLongEmail :: Gen Text+genLongEmail = do+  display <- genLocalPart+  localPart <- genLocalPart+  domain <- genDomain+  pure $+    mconcat+      [ display,+        " <",+        localPart,+        "@",+        domain,+        ">"+      ]++allowedChar :: Char -> Bool+allowedChar c = isAscii c || utf8NonAscii c++-- | Modified 'unicode' generator from Hedgehog.+unicode :: Gen Char+unicode =+  Gen.frequency+    [ (55296, Gen.element "!#$%&'*+-/=?^_`{|}~"),+      (55296, Gen.alphaNum),+      (8190, Gen.filter isPrint (Gen.enum (chr 0xc2) maxBound))+    ]
+ test/isemail.json view
@@ -0,0 +1,1 @@+[{"ietId":1,"ietAddr":"","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_NODOMAIN"},{"ietId":2,"ietAddr":"test","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_NODOMAIN"},{"ietId":3,"ietAddr":"@","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_NOLOCALPART"},{"ietId":4,"ietAddr":"test@","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_NODOMAIN"},{"ietId":5,"ietAddr":"test@io","ietCat":"ISEMAIL_VALID_CATEGORY","ietDia":"ISEMAIL_VALID"},{"ietId":6,"ietAddr":"@io","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_NOLOCALPART"},{"ietId":7,"ietAddr":"@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_NOLOCALPART"},{"ietId":8,"ietAddr":"test@iana.org","ietCat":"ISEMAIL_VALID_CATEGORY","ietDia":"ISEMAIL_VALID"},{"ietId":9,"ietAddr":"test@nominet.org.uk","ietCat":"ISEMAIL_VALID_CATEGORY","ietDia":"ISEMAIL_VALID"},{"ietId":10,"ietAddr":"test@about.museum","ietCat":"ISEMAIL_VALID_CATEGORY","ietDia":"ISEMAIL_VALID"},{"ietId":11,"ietAddr":"a@iana.org","ietCat":"ISEMAIL_VALID_CATEGORY","ietDia":"ISEMAIL_VALID"},{"ietId":12,"ietAddr":"test@e.com","ietCat":"ISEMAIL_DNSWARN","ietDia":"ISEMAIL_DNSWARN_NO_RECORD"},{"ietId":13,"ietAddr":"test@iana.a","ietCat":"ISEMAIL_DNSWARN","ietDia":"ISEMAIL_DNSWARN_NO_RECORD"},{"ietId":14,"ietAddr":"test.test@iana.org","ietCat":"ISEMAIL_VALID_CATEGORY","ietDia":"ISEMAIL_VALID"},{"ietId":15,"ietAddr":".test@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_DOT_START"},{"ietId":16,"ietAddr":"test.@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_DOT_END"},{"ietId":17,"ietAddr":"test..iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_CONSECUTIVEDOTS"},{"ietId":18,"ietAddr":"test_exa-mple.com","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_NODOMAIN"},{"ietId":19,"ietAddr":"!%23$%25&%60*+/=?%5E%60%7B%7C%7D~@iana.org","ietCat":"ISEMAIL_VALID_CATEGORY","ietDia":"ISEMAIL_VALID"},{"ietId":20,"ietAddr":"test%5C@test@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_EXPECTING_ATEXT"},{"ietId":21,"ietAddr":"123@iana.org","ietCat":"ISEMAIL_VALID_CATEGORY","ietDia":"ISEMAIL_VALID"},{"ietId":22,"ietAddr":"test@123.com","ietCat":"ISEMAIL_VALID_CATEGORY","ietDia":"ISEMAIL_VALID"},{"ietId":23,"ietAddr":"test@iana.123","ietCat":"ISEMAIL_RFC5321","ietDia":"ISEMAIL_RFC5321_TLDNUMERIC"},{"ietId":24,"ietAddr":"test@255.255.255.255","ietCat":"ISEMAIL_RFC5321","ietDia":"ISEMAIL_RFC5321_TLDNUMERIC"},{"ietId":25,"ietAddr":"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklm@iana.org","ietCat":"ISEMAIL_VALID_CATEGORY","ietDia":"ISEMAIL_VALID"},{"ietId":26,"ietAddr":"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklmn@iana.org","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_LOCAL_TOOLONG"},{"ietId":27,"ietAddr":"test@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.com","ietCat":"ISEMAIL_DNSWARN","ietDia":"ISEMAIL_DNSWARN_NO_RECORD"},{"ietId":28,"ietAddr":"test@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklm.com","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_LABEL_TOOLONG"},{"ietId":29,"ietAddr":"test@mason-dixon.com","ietCat":"ISEMAIL_VALID_CATEGORY","ietDia":"ISEMAIL_VALID"},{"ietId":30,"ietAddr":"test@-iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_DOMAINHYPHENSTART"},{"ietId":31,"ietAddr":"test@iana-.com","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_DOMAINHYPHENEND"},{"ietId":32,"ietAddr":"test@c--n.com","ietCat":"ISEMAIL_VALID_CATEGORY","ietDia":"ISEMAIL_VALID"},{"ietId":33,"ietAddr":"test@iana.co-uk","ietCat":"ISEMAIL_DNSWARN","ietDia":"ISEMAIL_DNSWARN_NO_RECORD"},{"ietId":34,"ietAddr":"test@.iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_DOT_START"},{"ietId":35,"ietAddr":"test@iana.org.","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_DOT_END"},{"ietId":36,"ietAddr":"test@iana..com","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_CONSECUTIVEDOTS"},{"ietId":37,"ietAddr":"a@a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v","ietCat":"ISEMAIL_DNSWARN","ietDia":"ISEMAIL_DNSWARN_NO_RECORD"},{"ietId":38,"ietAddr":"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklm@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghi","ietCat":"ISEMAIL_DNSWARN","ietDia":"ISEMAIL_DNSWARN_NO_RECORD"},{"ietId":39,"ietAddr":"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklm@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghij","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_TOOLONG"},{"ietId":40,"ietAddr":"a@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefg.hij","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_TOOLONG"},{"ietId":41,"ietAddr":"a@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefg.hijk","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_DOMAIN_TOOLONG"},{"ietId":42,"ietAddr":"%22test%22@iana.org","ietCat":"ISEMAIL_RFC5321","ietDia":"ISEMAIL_RFC5321_QUOTEDSTRING"},{"ietId":43,"ietAddr":"%22%22@iana.org","ietCat":"ISEMAIL_RFC5321","ietDia":"ISEMAIL_RFC5321_QUOTEDSTRING"},{"ietId":44,"ietAddr":"%22%22%22@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_EXPECTING_ATEXT"},{"ietId":45,"ietAddr":"%22%5Ca%22@iana.org","ietCat":"ISEMAIL_RFC5321","ietDia":"ISEMAIL_RFC5321_QUOTEDSTRING"},{"ietId":46,"ietAddr":"%22%5C%22%22@iana.org","ietCat":"ISEMAIL_RFC5321","ietDia":"ISEMAIL_RFC5321_QUOTEDSTRING"},{"ietId":47,"ietAddr":"%22%5C%22@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_UNCLOSEDQUOTEDSTR"},{"ietId":48,"ietAddr":"%22%5C%5C%22@iana.org","ietCat":"ISEMAIL_RFC5321","ietDia":"ISEMAIL_RFC5321_QUOTEDSTRING"},{"ietId":49,"ietAddr":"test%22@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_EXPECTING_ATEXT"},{"ietId":50,"ietAddr":"%22test@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_UNCLOSEDQUOTEDSTR"},{"ietId":51,"ietAddr":"%22test%22test@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_ATEXT_AFTER_QS"},{"ietId":52,"ietAddr":"test%22text%22@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_EXPECTING_ATEXT"},{"ietId":53,"ietAddr":"%22test%22%22test%22@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_EXPECTING_ATEXT"},{"ietId":54,"ietAddr":"%22test%22.%22test%22@iana.org","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_LOCALPART"},{"ietId":55,"ietAddr":"%22test%5C%20test%22@iana.org","ietCat":"ISEMAIL_RFC5321","ietDia":"ISEMAIL_RFC5321_QUOTEDSTRING"},{"ietId":56,"ietAddr":"%22test%22.test@iana.org","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_LOCALPART"},{"ietId":57,"ietAddr":"%22test%E2%90%80%22@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_EXPECTING_QTEXT"},{"ietId":58,"ietAddr":"%22test%5C%E2%90%80%22@iana.org","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_QP"},{"ietId":59,"ietAddr":"%22abcdefghijklmnopqrstuvwxyz%20abcdefghijklmnopqrstuvwxyz%20abcdefghj%22@iana.org","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_LOCAL_TOOLONG"},{"ietId":60,"ietAddr":"%22abcdefghijklmnopqrstuvwxyz%20abcdefghijklmnopqrstuvwxyz%20abcdefg%5Ch%22@iana.org","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_LOCAL_TOOLONG"},{"ietId":61,"ietAddr":"test@[255.255.255.255]","ietCat":"ISEMAIL_RFC5321","ietDia":"ISEMAIL_RFC5321_ADDRESSLITERAL"},{"ietId":62,"ietAddr":"test@a[255.255.255.255]","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_EXPECTING_ATEXT"},{"ietId":63,"ietAddr":"test@[255.255.255]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_DOMAINLITERAL"},{"ietId":64,"ietAddr":"test@[255.255.255.255.255]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_DOMAINLITERAL"},{"ietId":65,"ietAddr":"test@[255.255.255.256]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_DOMAINLITERAL"},{"ietId":66,"ietAddr":"test@[1111:2222:3333:4444:5555:6666:7777:8888]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_DOMAINLITERAL"},{"ietId":67,"ietAddr":"test@[IPv6:1111:2222:3333:4444:5555:6666:7777]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_IPV6_GRPCOUNT"},{"ietId":68,"ietAddr":"test@[IPv6:1111:2222:3333:4444:5555:6666:7777:8888]","ietCat":"ISEMAIL_RFC5321","ietDia":"ISEMAIL_RFC5321_ADDRESSLITERAL"},{"ietId":69,"ietAddr":"test@[IPv6:1111:2222:3333:4444:5555:6666:7777:8888:9999]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_IPV6_GRPCOUNT"},{"ietId":70,"ietAddr":"test@[IPv6:1111:2222:3333:4444:5555:6666:7777:888G]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_IPV6_BADCHAR"},{"ietId":71,"ietAddr":"test@[IPv6:1111:2222:3333:4444:5555:6666::8888]","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_RFC5321_IPV6DEPRECATED"},{"ietId":72,"ietAddr":"test@[IPv6:1111:2222:3333:4444:5555::8888]","ietCat":"ISEMAIL_RFC5321","ietDia":"ISEMAIL_RFC5321_ADDRESSLITERAL"},{"ietId":73,"ietAddr":"test@[IPv6:1111:2222:3333:4444:5555:6666::7777:8888]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_IPV6_MAXGRPS"},{"ietId":74,"ietAddr":"test@[IPv6::3333:4444:5555:6666:7777:8888]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_IPV6_COLONSTRT"},{"ietId":75,"ietAddr":"test@[IPv6:::3333:4444:5555:6666:7777:8888]","ietCat":"ISEMAIL_RFC5321","ietDia":"ISEMAIL_RFC5321_ADDRESSLITERAL"},{"ietId":76,"ietAddr":"test@[IPv6:1111::4444:5555::8888]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_IPV6_2X2XCOLON"},{"ietId":77,"ietAddr":"test@[IPv6:::]","ietCat":"ISEMAIL_RFC5321","ietDia":"ISEMAIL_RFC5321_ADDRESSLITERAL"},{"ietId":78,"ietAddr":"test@[IPv6:1111:2222:3333:4444:5555:255.255.255.255]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_IPV6_GRPCOUNT"},{"ietId":79,"ietAddr":"test@[IPv6:1111:2222:3333:4444:5555:6666:255.255.255.255]","ietCat":"ISEMAIL_RFC5321","ietDia":"ISEMAIL_RFC5321_ADDRESSLITERAL"},{"ietId":80,"ietAddr":"test@[IPv6:1111:2222:3333:4444:5555:6666:7777:255.255.255.255]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_IPV6_GRPCOUNT"},{"ietId":81,"ietAddr":"test@[IPv6:1111:2222:3333:4444::255.255.255.255]","ietCat":"ISEMAIL_RFC5321","ietDia":"ISEMAIL_RFC5321_ADDRESSLITERAL"},{"ietId":82,"ietAddr":"test@[IPv6:1111:2222:3333:4444:5555:6666::255.255.255.255]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_IPV6_MAXGRPS"},{"ietId":83,"ietAddr":"test@[IPv6:1111:2222:3333:4444:::255.255.255.255]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_IPV6_2X2XCOLON"},{"ietId":84,"ietAddr":"test@[IPv6::255.255.255.255]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_IPV6_COLONSTRT"},{"ietId":85,"ietAddr":"%20test%20@iana.org","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_CFWS_NEAR_AT"},{"ietId":86,"ietAddr":"test@%20iana%20.com","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_CFWS_NEAR_AT"},{"ietId":87,"ietAddr":"test%20.%20test@iana.org","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_FWS"},{"ietId":88,"ietAddr":"%E2%90%8D%E2%90%8A%20test@iana.org","ietCat":"ISEMAIL_CFWS","ietDia":"ISEMAIL_CFWS_FWS"},{"ietId":89,"ietAddr":"%E2%90%8D%E2%90%8A%20%E2%90%8D%E2%90%8A%20test@iana.org","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_FWS"},{"ietId":90,"ietAddr":"(comment)test@iana.org","ietCat":"ISEMAIL_CFWS","ietDia":"ISEMAIL_CFWS_COMMENT"},{"ietId":91,"ietAddr":"((comment)test@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_UNCLOSEDCOMMENT"},{"ietId":92,"ietAddr":"(comment(comment))test@iana.org","ietCat":"ISEMAIL_CFWS","ietDia":"ISEMAIL_CFWS_COMMENT"},{"ietId":93,"ietAddr":"test@(comment)iana.org","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_CFWS_NEAR_AT"},{"ietId":94,"ietAddr":"test(comment)test@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_ATEXT_AFTER_CFWS"},{"ietId":95,"ietAddr":"test@(comment)[255.255.255.255]","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_CFWS_NEAR_AT"},{"ietId":96,"ietAddr":"(comment)abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklm@iana.org","ietCat":"ISEMAIL_CFWS","ietDia":"ISEMAIL_CFWS_COMMENT"},{"ietId":97,"ietAddr":"test@(comment)abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.com","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_CFWS_NEAR_AT"},{"ietId":98,"ietAddr":"(comment)test@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghik.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghik.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstu","ietCat":"ISEMAIL_CFWS","ietDia":"ISEMAIL_CFWS_COMMENT"},{"ietId":99,"ietAddr":"test@iana.org%E2%90%8A","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_EXPECTING_ATEXT"},{"ietId":100,"ietAddr":"test@xn--hxajbheg2az3al.xn--jxalpdlp","ietCat":"ISEMAIL_VALID_CATEGORY","ietDia":"ISEMAIL_VALID"},{"ietId":101,"ietAddr":"xn--test@iana.org","ietCat":"ISEMAIL_VALID_CATEGORY","ietDia":"ISEMAIL_VALID"},{"ietId":102,"ietAddr":"test@iana.org-","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_DOMAINHYPHENEND"},{"ietId":103,"ietAddr":"%22test@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_UNCLOSEDQUOTEDSTR"},{"ietId":104,"ietAddr":"(test@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_UNCLOSEDCOMMENT"},{"ietId":105,"ietAddr":"test@(iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_UNCLOSEDCOMMENT"},{"ietId":106,"ietAddr":"test@[1.2.3.4","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_UNCLOSEDDOMLIT"},{"ietId":107,"ietAddr":"%22test%5C%22@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_UNCLOSEDQUOTEDSTR"},{"ietId":108,"ietAddr":"(comment%5C)test@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_UNCLOSEDCOMMENT"},{"ietId":109,"ietAddr":"test@iana.org(comment%5C)","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_UNCLOSEDCOMMENT"},{"ietId":110,"ietAddr":"test@iana.org(comment%5C","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_BACKSLASHEND"},{"ietId":112,"ietAddr":"test@[RFC-5322-domain-literal]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_DOMAINLITERAL"},{"ietId":113,"ietAddr":"test@[RFC-5322]-domain-literal]","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_ATEXT_AFTER_DOMLIT"},{"ietId":114,"ietAddr":"test@[RFC-5322-[domain-literal]","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_EXPECTING_DTEXT"},{"ietId":115,"ietAddr":"test@[RFC-5322-%5C%E2%90%87-domain-literal]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_DOMLIT_OBSDTEXT"},{"ietId":116,"ietAddr":"test@[RFC-5322-%5C%E2%90%89-domain-literal]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_DOMLIT_OBSDTEXT"},{"ietId":117,"ietAddr":"test@[RFC-5322-%5C]-domain-literal]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_DOMLIT_OBSDTEXT"},{"ietId":118,"ietAddr":"test@[RFC-5322-domain-literal%5C]","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_UNCLOSEDDOMLIT"},{"ietId":119,"ietAddr":"test@[RFC-5322-domain-literal%5C","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_BACKSLASHEND"},{"ietId":120,"ietAddr":"test@[RFC%205322%20domain%20literal]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_DOMAINLITERAL"},{"ietId":121,"ietAddr":"test@[RFC-5322-domain-literal]%20(comment)","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_DOMAINLITERAL"},{"ietId":122,"ietAddr":"%7F@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_EXPECTING_ATEXT"},{"ietId":123,"ietAddr":"test@%7F.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_EXPECTING_ATEXT"},{"ietId":124,"ietAddr":"%22%7F%22@iana.org","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_QTEXT"},{"ietId":125,"ietAddr":"%22%5C%7F%22@iana.org","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_QP"},{"ietId":126,"ietAddr":"(%7F)test@iana.org","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_CTEXT"},{"ietId":127,"ietAddr":"test@iana.org%E2%90%8D","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_CR_NO_LF"},{"ietId":128,"ietAddr":"%E2%90%8Dtest@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_CR_NO_LF"},{"ietId":129,"ietAddr":"%22%E2%90%8Dtest%22@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_CR_NO_LF"},{"ietId":130,"ietAddr":"(%E2%90%8D)test@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_CR_NO_LF"},{"ietId":131,"ietAddr":"test@iana.org(%E2%90%8D)","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_CR_NO_LF"},{"ietId":132,"ietAddr":"%E2%90%8Atest@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_EXPECTING_ATEXT"},{"ietId":133,"ietAddr":"%22%E2%90%8A%22@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_EXPECTING_QTEXT"},{"ietId":134,"ietAddr":"%22%5C%E2%90%8A%22@iana.org","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_QP"},{"ietId":135,"ietAddr":"(%E2%90%8A)test@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_EXPECTING_CTEXT"},{"ietId":136,"ietAddr":"%E2%90%87@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_EXPECTING_ATEXT"},{"ietId":137,"ietAddr":"test@%E2%90%87.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_EXPECTING_ATEXT"},{"ietId":138,"ietAddr":"%22%E2%90%87%22@iana.org","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_QTEXT"},{"ietId":139,"ietAddr":"%22%5C%E2%90%87%22@iana.org","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_QP"},{"ietId":140,"ietAddr":"(%E2%90%87)test@iana.org","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_CTEXT"},{"ietId":141,"ietAddr":"%E2%90%8D%E2%90%8Atest@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_FWS_CRLF_END"},{"ietId":142,"ietAddr":"%E2%90%8D%E2%90%8A%20%E2%90%8D%E2%90%8Atest@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_FWS_CRLF_END"},{"ietId":143,"ietAddr":"%20%E2%90%8D%E2%90%8Atest@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_FWS_CRLF_END"},{"ietId":144,"ietAddr":"%20%E2%90%8D%E2%90%8A%20test@iana.org","ietCat":"ISEMAIL_CFWS","ietDia":"ISEMAIL_CFWS_FWS"},{"ietId":145,"ietAddr":"%20%E2%90%8D%E2%90%8A%20%E2%90%8D%E2%90%8Atest@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_FWS_CRLF_END"},{"ietId":146,"ietAddr":"%20%E2%90%8D%E2%90%8A%E2%90%8D%E2%90%8Atest@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_FWS_CRLF_X2"},{"ietId":147,"ietAddr":"%20%E2%90%8D%E2%90%8A%E2%90%8D%E2%90%8A%20test@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_FWS_CRLF_X2"},{"ietId":148,"ietAddr":"test@iana.org%E2%90%8D%E2%90%8A%20","ietCat":"ISEMAIL_CFWS","ietDia":"ISEMAIL_CFWS_FWS"},{"ietId":149,"ietAddr":"test@iana.org%E2%90%8D%E2%90%8A%20%E2%90%8D%E2%90%8A%20","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_FWS"},{"ietId":150,"ietAddr":"test@iana.org%E2%90%8D%E2%90%8A","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_FWS_CRLF_END"},{"ietId":151,"ietAddr":"test@iana.org%E2%90%8D%E2%90%8A%20%E2%90%8D%E2%90%8A","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_FWS_CRLF_END"},{"ietId":152,"ietAddr":"test@iana.org%20%E2%90%8D%E2%90%8A","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_FWS_CRLF_END"},{"ietId":153,"ietAddr":"test@iana.org%20%E2%90%8D%E2%90%8A%20","ietCat":"ISEMAIL_CFWS","ietDia":"ISEMAIL_CFWS_FWS"},{"ietId":154,"ietAddr":"test@iana.org%20%E2%90%8D%E2%90%8A%20%E2%90%8D%E2%90%8A","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_FWS_CRLF_END"},{"ietId":155,"ietAddr":"test@iana.org%20%E2%90%8D%E2%90%8A%E2%90%8D%E2%90%8A","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_FWS_CRLF_X2"},{"ietId":156,"ietAddr":"test@iana.org%20%E2%90%8D%E2%90%8A%E2%90%8D%E2%90%8A%20","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_FWS_CRLF_X2"},{"ietId":157,"ietAddr":"%20test@iana.org","ietCat":"ISEMAIL_CFWS","ietDia":"ISEMAIL_CFWS_FWS"},{"ietId":158,"ietAddr":"test@iana.org%20","ietCat":"ISEMAIL_CFWS","ietDia":"ISEMAIL_CFWS_FWS"},{"ietId":159,"ietAddr":"test@[IPv6:1::2:]","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_IPV6_COLONEND"},{"ietId":160,"ietAddr":"%22test%5C%C2%A9%22@iana.org","ietCat":"ISEMAIL_ERR","ietDia":"ISEMAIL_ERR_EXPECTING_QPAIR"},{"ietId":161,"ietAddr":"test@iana/icann.org","ietCat":"ISEMAIL_RFC5322","ietDia":"ISEMAIL_RFC5322_DOMAIN"},{"ietId":165,"ietAddr":"test.(comment)test@iana.org","ietCat":"ISEMAIL_DEPREC","ietDia":"ISEMAIL_DEPREC_COMMENT"},{"ietId":166,"ietAddr":"test@org","ietCat":"ISEMAIL_RFC5321","ietDia":"ISEMAIL_RFC5321_TLD"},{"ietId":167,"ietAddr":"test@test.com","ietCat":"ISEMAIL_DNSWARN","ietDia":"ISEMAIL_DNSWARN_NO_MX_RECORD"},{"ietId":168,"ietAddr":"test@nic.no","ietCat":"ISEMAIL_DNSWARN","ietDia":"ISEMAIL_DNSWARN_NO_RECORD"}]