From 0d6cfa67e27462dc4a934a3d89e9f151654f5fcc Mon Sep 17 00:00:00 2001 From: Orion Kindel Date: Wed, 6 Dec 2023 14:27:41 -0600 Subject: [PATCH] feat: initial commit --- bun.lockb | Bin 53117 -> 54489 bytes package.json | 2 +- spago.yaml | 12 +++++-- src/Data.Email.purs | 43 +++++++++++++++++++++++ src/Effect.Email.js | 4 +++ src/Effect.Email.purs | 79 ++++++++++++++++++++++++++++++++++++++++++ test/Test.Main.purs | 77 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 213 insertions(+), 4 deletions(-) create mode 100644 src/Data.Email.purs create mode 100644 src/Effect.Email.js create mode 100644 src/Effect.Email.purs create mode 100644 test/Test.Main.purs diff --git a/bun.lockb b/bun.lockb index c42d05ceb16556e06bd80e7e360bf13128ae05e8..7e7e2ea3e7286b8f7e83f7fff69f98f7200146e4 100755 GIT binary patch delta 9332 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 Ux{z7widQ#Lc6;)Tw&T738#Tp8y8r+H delta 8624 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 diff --git a/package.json b/package.json index e446309..cfb546a 100644 --- a/package.json +++ b/package.json @@ -14,5 +14,5 @@ "peerDependencies": { "typescript": "^5.0.0" }, - "dependencies": {} + "dependencies": { "qed-mail": "^1.0.2" } } diff --git a/spago.yaml b/spago.yaml index a8c16f6..b91fb81 100644 --- a/spago.yaml +++ b/spago.yaml @@ -1,19 +1,25 @@ package: dependencies: - - prelude - aff + - aff-promise + - console - effect - either - - maybe - foldable-traversable - - console + - maybe - newtype + - nullable + - prelude - strings - stringutils - transformers - tuples - typelevel-prelude name: project + test: + dependencies: + - spec + main: Test.Main workspace: extra_packages: {} package_set: diff --git a/src/Data.Email.purs b/src/Data.Email.purs new file mode 100644 index 0000000..d36ffee --- /dev/null +++ b/src/Data.Email.purs @@ -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 diff --git a/src/Effect.Email.js b/src/Effect.Email.js new file mode 100644 index 0000000..444eda2 --- /dev/null +++ b/src/Effect.Email.js @@ -0,0 +1,4 @@ +import { checkEmail } from 'qed-mail' + +/** @type {(_: string) => () => Promise} */ +export const checkEmailImpl = s => () => checkEmail(s) diff --git a/src/Effect.Email.purs b/src/Effect.Email.purs new file mode 100644 index 0000000..be425ea --- /dev/null +++ b/src/Effect.Email.purs @@ -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' diff --git a/test/Test.Main.purs b/test/Test.Main.purs new file mode 100644 index 0000000..07d2c87 --- /dev/null +++ b/test/Test.Main.purs @@ -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