From: Michael Orlitzky Date: Thu, 25 Apr 2024 00:36:54 +0000 (-0400) Subject: email-validator.cabal: bump to version 1.1.0 X-Git-Tag: 1.1.0^0 X-Git-Url: http://gitweb.michael.orlitzky.com/?p=email-validator.git;a=commitdiff_plain;h=HEAD;hp=fc08ccc3a797f2f5381de72bfbd787f7ce279c79 email-validator.cabal: bump to version 1.1.0 --- diff --git a/doc/COPYING b/doc/COPYING new file mode 100644 index 0000000..dcca366 --- /dev/null +++ b/doc/COPYING @@ -0,0 +1,15 @@ +email-validator: basic syntax and deliverability checks on email addresses +Copyright (C) 2024 Michael Orlitzky + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/doc/man1/email-validator.1 b/doc/man1/email-validator.1 index 1e0e144..605b570 100644 --- a/doc/man1/email-validator.1 +++ b/doc/man1/email-validator.1 @@ -33,10 +33,12 @@ grammar (see the \fB\-\-rfc5322\fR option). .IP \[bu] Confirmation of the existence of an \fIMX\fR record for the domain -part of the address. This is not required; in fact many domains accept -mail via an \fIA\fR record for e.g. example.com which is used in lieu -of an \fIMX\fR record. This behavior can be controlled via the -\fR\-\-accept\-a\fR flag. +part of the address. NULLMX (RFC7505) records are not accepted. This +is not required; in fact many domains accept mail via an \fIA\fR +record for (say) example.com which is used in lieu of an \fIMX\fR +record. This behavior can be controlled via the \fR\-\-accept\-a\fR +flag, but note that \fR\-\-accept\-a\fR is ignored for domains that +have NULLMX records. .P These checks are performed in parallel using the number of available diff --git a/email-validator.cabal b/email-validator.cabal index c899d8c..94c14f4 100644 --- a/email-validator.cabal +++ b/email-validator.cabal @@ -1,15 +1,16 @@ cabal-version: 3.0 name: email-validator -version: 1.0.0 +version: 1.1.0 author: Michael Orlitzky maintainer: Michael Orlitzky -homepage: http://michael.orlitzky.com/code/email-validator.xhtml +homepage: https://michael.orlitzky.com/code/email-validator.xhtml bug-reports: mailto:michael@orlitzky.com category: Utils -license: AGPL-3.0-only +license: AGPL-3.0-or-later license-file: doc/LICENSE build-type: Simple extra-source-files: + doc/COPYING doc/man1/email-validator.1 synopsis: Perform basic syntax and deliverability checks on email addresses. @@ -18,33 +19,13 @@ description: * Ensuring that the length of local and domain parts is within the RFC-specified limits. - * A syntax check using a regular expression, or the full RFC 5322 - grammar (see the @--rfc5322@ option). - - * Confirmation of the existence of an @MX@ record for the domain part of - the address. This is not required; in fact many domains accept mail - via an @A@ record for e.g. example.com which is used in lieu of an @MX@ - record. This behavior can be controlled via the @--accept-a@ flag. - - These checks are performed in parallel using the number of available - threads. To increase the number of threads, you can pass the - appropriate flag to the GHC runtime. - - This will set the number of threads to 25: - - @ - $ email-validator +RTS -N25 < addresses.csv - @ - - /Input/ - - The @input@ (via stdin) should be a list of email addresses, - one per line. Empty lines will be ignored. - - /Output/ + grammar. + * Confirmation of valid @MX@ records (or, optionally, @A@ + records) for the domain. - Valid email addresses will be written to stdout, one per line. + A complete description, options, and examples can be found in the + man page. executable email-validator @@ -110,5 +91,5 @@ test-suite doctests source-repository head type: git - location: http://gitweb.michael.orlitzky.com/email-validator.git + location: https://gitweb.michael.orlitzky.com/email-validator.git branch: master diff --git a/makefile b/makefile index e77b227..5052789 100644 --- a/makefile +++ b/makefile @@ -13,6 +13,7 @@ HCFLAGS += -Weverything \ -Wno-prepositive-qualified-module \ -Wno-missing-safe-haskell-mode \ -Wno-missing-deriving-strategies \ + -Wno-missing-kind-signatures \ -rtsopts \ -threaded diff --git a/src/Main.hs b/src/Main.hs index 1cc4c73..ab5f93a 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -41,28 +41,63 @@ import EmailAddress( resolv_conf :: ResolvConf resolv_conf = defaultResolvConf { resolvTimeout = 10 * 1000 * 1000 } --- | A list of common domains, there's no need to waste MX lookups --- on these. +-- | A list of common domains, there's no need to waste MX lookups on +-- these. This is a very limited list; I don't want to be in the +-- business of monitoring a million domains for MX record updates. common_domains :: [Domain] common_domains = map BS.pack [ "aol.com", "comcast.net", + "cox.net", "gmail.com", + "gmx.de", + "googlemail.com", + "hotmail.com", + "icloud.com", + "live.com", + "me.com", "msn.com", + "outlook.com", + "proton.me", + "protonmail.ch", + "protonmail.com", "yahoo.com", "verizon.net" ] -- | Check whether the given domain has a valid MX record. -validate_mx :: Resolver -> Domain -> IO Bool +-- +-- NULLMX (RFC7505) records consisting of a single period must not +-- be accepted. Moreover, the existence of a NULLMX must be reported +-- back to the caller because the whole point of a NULLMX is that +-- its existence should preempt an @A@ record check. We abuse the +-- return type for this, and return @Nothing@ in the event of a +-- NULLMX. Otherwise we return @Just True@ or @Just False@ to +-- indicate the existence (or not) of MX records. +-- +-- RFC7505 states that a domain MUST NOT have any other MX records +-- if it has a NULLMX record. We enforce this. If you have a NULLMX +-- record and some other MX record, we consider the set invalid. +-- +validate_mx :: Resolver -> Domain -> IO (Maybe Bool) validate_mx resolver domain - | domain `elem` common_domains = return True + | domain `elem` common_domains = return $ Just True | otherwise = do result <- lookupMX resolver domain case result of - -- A list of one or more elements? - Right (_:_) -> return True - _ -> return False - + Left _ -> + return $ Just False + Right mxs -> + case mxs of + [] -> return $ Just False + _ -> if any (is_null) mxs + then return Nothing + else return $ Just True + where + nullmx :: Domain + nullmx = BS.pack "." + + is_null :: (Domain,Int) -> Bool + is_null (mx,prio) = mx == nullmx && prio == 0 -- | Check whether the given domain has a valid A record. validate_a :: Resolver -> Domain -> IO Bool @@ -76,26 +111,33 @@ validate_a resolver domain -- | Validate an email address by doing some simple syntax checks and --- (if those fail) an MX lookup. We don't count an A record as a mail --- exchanger. +-- (if those fail) an MX lookup. We don't count an @A@ record as a mail +-- exchanger unless @accept_a@ is True. And even then, the existence +-- of a NULLMX record will preclude the acceptance of an @A@ record. +-- The domain @example.com@ is a great test case for this behavior. validate :: Resolver -> Bool -> Bool -> Address -> IO (Address, Bool) validate resolver accept_a rfc5322 address = do let valid_syntax = validate_syntax rfc5322 address if valid_syntax then do let (_,domain) = parts address mx_result <- validate_mx resolver domain - if mx_result - then return (address, True) - else - if accept_a - then do - a_result <- validate_a resolver domain - return (address, a_result) - else - return (address, False) - else - return (address, False) - + case mx_result of + Nothing -> + -- NULLMX, don't fall back to 'A' records under any + -- circumstances. + return (address, False) + Just mxr -> + if mxr + then return (address, True) + else + if accept_a + then do + a_result <- validate_a resolver domain + return (address, a_result) + else + return (address, False) + else + return (address, False) main :: IO ()