generated from tpl/purs
feat: initial commit
This commit is contained in:
parent
fda84ce85b
commit
0d6cfa67e2
@ -14,5 +14,5 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {}
|
"dependencies": { "qed-mail": "^1.0.2" }
|
||||||
}
|
}
|
||||||
|
12
spago.yaml
12
spago.yaml
@ -1,19 +1,25 @@
|
|||||||
package:
|
package:
|
||||||
dependencies:
|
dependencies:
|
||||||
- prelude
|
|
||||||
- aff
|
- aff
|
||||||
|
- aff-promise
|
||||||
|
- console
|
||||||
- effect
|
- effect
|
||||||
- either
|
- either
|
||||||
- maybe
|
|
||||||
- foldable-traversable
|
- foldable-traversable
|
||||||
- console
|
- maybe
|
||||||
- newtype
|
- newtype
|
||||||
|
- nullable
|
||||||
|
- prelude
|
||||||
- strings
|
- strings
|
||||||
- stringutils
|
- stringutils
|
||||||
- transformers
|
- transformers
|
||||||
- tuples
|
- tuples
|
||||||
- typelevel-prelude
|
- typelevel-prelude
|
||||||
name: project
|
name: project
|
||||||
|
test:
|
||||||
|
dependencies:
|
||||||
|
- spec
|
||||||
|
main: Test.Main
|
||||||
workspace:
|
workspace:
|
||||||
extra_packages: {}
|
extra_packages: {}
|
||||||
package_set:
|
package_set:
|
||||||
|
43
src/Data.Email.purs
Normal file
43
src/Data.Email.purs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
module Data.Email (Email, parse, toString, username, domain) where
|
||||||
|
|
||||||
|
import Prelude
|
||||||
|
|
||||||
|
import Control.Monad.Error.Class (throwError)
|
||||||
|
import Data.Array as Array
|
||||||
|
import Data.Either (Either)
|
||||||
|
import Data.Maybe (fromMaybe)
|
||||||
|
import Data.Newtype (wrap)
|
||||||
|
import Data.String as String
|
||||||
|
import Data.String.Regex as Regex
|
||||||
|
import Data.String.Regex.Flags as Regex.Flag
|
||||||
|
|
||||||
|
-- | An email address
|
||||||
|
data Email = Email String
|
||||||
|
|
||||||
|
instance Show Email where
|
||||||
|
show (Email e) = "(Email " <> e <> ")"
|
||||||
|
|
||||||
|
instance Eq Email where
|
||||||
|
eq (Email a) (Email b) = a == b
|
||||||
|
|
||||||
|
-- | https://emailregex.com/
|
||||||
|
regex :: String
|
||||||
|
regex = """^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$"""
|
||||||
|
|
||||||
|
toString :: Email -> String
|
||||||
|
toString (Email e) = e
|
||||||
|
|
||||||
|
-- | Username portion of the email address
|
||||||
|
username :: Email -> String
|
||||||
|
username (Email raw) = fromMaybe "" $ Array.head $ String.split (wrap "@") raw
|
||||||
|
|
||||||
|
-- | Domain portion of the email address
|
||||||
|
domain :: Email -> String
|
||||||
|
domain (Email raw) = fromMaybe "" $ Array.last $ String.split (wrap "@") raw
|
||||||
|
|
||||||
|
-- | Parse a string as an email address
|
||||||
|
parse :: String -> Either String Email
|
||||||
|
parse raw = do
|
||||||
|
re <- Regex.regex regex Regex.Flag.noFlags
|
||||||
|
when (not $ Regex.test re raw) $ throwError $ "Email address invalid: <" <> raw <> ">"
|
||||||
|
pure $ Email raw
|
4
src/Effect.Email.js
Normal file
4
src/Effect.Email.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { checkEmail } from 'qed-mail'
|
||||||
|
|
||||||
|
/** @type {(_: string) => () => Promise<import('qed-mail/src/types.js').Result>} */
|
||||||
|
export const checkEmailImpl = s => () => checkEmail(s)
|
79
src/Effect.Email.purs
Normal file
79
src/Effect.Email.purs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
module Effect.Email (EmailError(..), emailErrorToString, deliverable) where
|
||||||
|
|
||||||
|
import Prelude
|
||||||
|
|
||||||
|
import Control.Monad.Cont (lift)
|
||||||
|
import Control.Monad.Error.Class (throwError)
|
||||||
|
import Control.Monad.Except (ExceptT)
|
||||||
|
import Control.Promise (Promise)
|
||||||
|
import Control.Promise as Promise
|
||||||
|
import Data.Array as Array
|
||||||
|
import Data.Email (Email)
|
||||||
|
import Data.Email as Email
|
||||||
|
import Data.Eq.Generic (genericEq)
|
||||||
|
import Data.Generic.Rep (class Generic)
|
||||||
|
import Data.Maybe (maybe)
|
||||||
|
import Data.Nullable (Nullable)
|
||||||
|
import Data.Nullable as Nullable
|
||||||
|
import Data.Show.Generic (genericShow)
|
||||||
|
import Data.Traversable (for_)
|
||||||
|
import Effect (Effect)
|
||||||
|
import Effect.Aff (Aff)
|
||||||
|
import Type.Function (type ($))
|
||||||
|
|
||||||
|
foreign import checkEmailImpl :: String -> Effect $ Promise $ QEDResult
|
||||||
|
|
||||||
|
data EmailError
|
||||||
|
= EmailUnreachable Email
|
||||||
|
| EmailSyntaxInvalid Email
|
||||||
|
| EmailMXInvalid Email
|
||||||
|
| EmailMXNoRecords Email
|
||||||
|
| EmailSMTPInvalid Email
|
||||||
|
| EmailSMTPError Email String
|
||||||
|
|
||||||
|
emailErrorToString :: EmailError -> String
|
||||||
|
emailErrorToString (EmailUnreachable e) = "Email <" <> Email.toString e <> "> is unreachable"
|
||||||
|
emailErrorToString (EmailSyntaxInvalid e) = "Email <" <> Email.toString e <> "> is invalid"
|
||||||
|
emailErrorToString (EmailMXInvalid e) = "Email <" <> Email.toString e <> "> domain has invalid MX record"
|
||||||
|
emailErrorToString (EmailMXNoRecords e) = "Email <" <> Email.toString e <> "> domain has no MX records"
|
||||||
|
emailErrorToString (EmailSMTPInvalid e) = "Email <" <> Email.toString e <> "> domain has invalid SMTP configuration"
|
||||||
|
emailErrorToString (EmailSMTPError e err) = "Email <" <> Email.toString e <> "> encountered SMTP error: " <> err
|
||||||
|
|
||||||
|
derive instance Generic EmailError _
|
||||||
|
instance Eq EmailError where
|
||||||
|
eq = genericEq
|
||||||
|
|
||||||
|
instance Show EmailError where
|
||||||
|
show = genericShow
|
||||||
|
|
||||||
|
type QEDResult =
|
||||||
|
{ email :: String
|
||||||
|
, reachable :: Boolean
|
||||||
|
, syntax ::
|
||||||
|
{ valid :: Boolean
|
||||||
|
, username :: Nullable String
|
||||||
|
, domain :: Nullable String
|
||||||
|
}
|
||||||
|
, mx ::
|
||||||
|
{ valid :: Boolean
|
||||||
|
, mxRecords :: Nullable $ Array { priority :: Number, exchange :: String }
|
||||||
|
}
|
||||||
|
, smtp ::
|
||||||
|
{ valid :: Boolean
|
||||||
|
, error :: Nullable String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deliverable :: Email -> ExceptT EmailError Aff Email
|
||||||
|
deliverable email' = do
|
||||||
|
let email = Email.toString email'
|
||||||
|
|
||||||
|
{ reachable, syntax, mx, smtp } <- lift $ Promise.toAffE $ checkEmailImpl email
|
||||||
|
when (not reachable) $ throwError $ EmailUnreachable email'
|
||||||
|
when (not syntax.valid) $ throwError $ EmailSyntaxInvalid email'
|
||||||
|
when (not mx.valid) $ throwError $ EmailMXInvalid email'
|
||||||
|
when (not smtp.valid) $ throwError $ EmailSMTPInvalid email'
|
||||||
|
when (maybe true Array.null $ Nullable.toMaybe $ mx.mxRecords) $ throwError $ EmailMXNoRecords email'
|
||||||
|
for_ (Nullable.toMaybe $ smtp.error) $ throwError <<< EmailSMTPError email'
|
||||||
|
|
||||||
|
pure email'
|
77
test/Test.Main.purs
Normal file
77
test/Test.Main.purs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
module Test.Main where
|
||||||
|
|
||||||
|
import Prelude
|
||||||
|
|
||||||
|
import Control.Monad.Error.Class (class MonadThrow, liftEither, throwError, try)
|
||||||
|
import Control.Monad.Except (runExceptT)
|
||||||
|
import Data.Bifunctor (lmap)
|
||||||
|
import Data.Either (Either(..), isRight)
|
||||||
|
import Data.Email (Email)
|
||||||
|
import Data.Email as Email
|
||||||
|
import Data.Traversable (for_)
|
||||||
|
import Data.Tuple.Nested ((/\))
|
||||||
|
import Effect (Effect)
|
||||||
|
import Effect.Aff (launchAff_)
|
||||||
|
import Effect.Email as Effect.Email
|
||||||
|
import Effect.Exception (Error, error)
|
||||||
|
import Test.Spec (describe, it)
|
||||||
|
import Test.Spec.Assertions (shouldEqual)
|
||||||
|
import Test.Spec.Reporter (consoleReporter)
|
||||||
|
import Test.Spec.Runner (runSpec)
|
||||||
|
|
||||||
|
emailParse :: forall m. MonadThrow Error m => String -> m Email
|
||||||
|
emailParse = liftEither <<< lmap error <<< Email.parse
|
||||||
|
|
||||||
|
main :: Effect Unit
|
||||||
|
main = launchAff_ $ runSpec [ consoleReporter ] do
|
||||||
|
describe "Data.Email" do
|
||||||
|
describe "parse" do
|
||||||
|
for_
|
||||||
|
[ "orion@orionkindel.com" /\ true
|
||||||
|
, "foo.bar@gmail.com" /\ true
|
||||||
|
, "foo.bar123@gmail.com" /\ true
|
||||||
|
, "foo-bar@gmail.com" /\ true
|
||||||
|
, "foo_bar@gmail.com" /\ true
|
||||||
|
, "foo_@gmail.com" /\ true
|
||||||
|
, "foo.bar+1@gmail.com" /\ true
|
||||||
|
, "_foo@gmail.com" /\ true
|
||||||
|
, "foo,bar@gmail.com" /\ false
|
||||||
|
, "foo@gmail" /\ false
|
||||||
|
, "foo@" /\ false
|
||||||
|
, "foo.com" /\ false
|
||||||
|
, "@foo.com" /\ false
|
||||||
|
]
|
||||||
|
\(e /\ pass) ->
|
||||||
|
it
|
||||||
|
((if pass then "pass" else "fail") <> " > `" <> e <> "`")
|
||||||
|
(isRight (Email.parse e) `shouldEqual` pass)
|
||||||
|
describe "toString" do
|
||||||
|
for_
|
||||||
|
[ "orion@orionkindel.com"
|
||||||
|
, "foo.bar@gmail.com"
|
||||||
|
, "foo.bar123@gmail.com"
|
||||||
|
, "foo-bar@gmail.com"
|
||||||
|
, "foo_bar@gmail.com"
|
||||||
|
, "foo_@gmail.com"
|
||||||
|
, "foo.bar+1@gmail.com"
|
||||||
|
, "_foo@gmail.com"
|
||||||
|
]
|
||||||
|
\e -> it ("`" <> e <> "`") $ flip shouldEqual e <$> Email.toString =<< emailParse e
|
||||||
|
describe "Effect.Email" do
|
||||||
|
describe "deliverable" do
|
||||||
|
for_
|
||||||
|
[ "cakekindel@gmail.com" /\ true
|
||||||
|
, "george@thunderstrike.ai" /\ true
|
||||||
|
, "foobarbaz@thunderstrike.ai" /\ false
|
||||||
|
]
|
||||||
|
\(e /\ shouldPass) ->
|
||||||
|
it ((if shouldPass then "pass" else "fail") <> " > `" <> e <> "`") do
|
||||||
|
email <- emailParse e
|
||||||
|
result <- runExceptT $ Effect.Email.deliverable email
|
||||||
|
case result of
|
||||||
|
Right _
|
||||||
|
| not shouldPass -> throwError $ error "deliverable did not fail but should have!"
|
||||||
|
| otherwise -> pure unit
|
||||||
|
Left err
|
||||||
|
| shouldPass -> throwError $ error $ Effect.Email.emailErrorToString err
|
||||||
|
| otherwise -> pure unit
|
Loading…
Reference in New Issue
Block a user