--- /dev/null
+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 <https://www.gnu.org/licenses/>.
cabal-version: 3.0
name: email-validator
-version: 1.0.0
+version: 1.1.0
author: Michael Orlitzky
maintainer: Michael Orlitzky <michael@orlitzky.com>
-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.
* 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
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
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
-- | 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 ()