summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorzytzagoo <zytzagoo@gmail.com>2017-11-10 16:20:59 +0100
committerzytzagoo <zytzagoo@gmail.com>2017-11-10 16:20:59 +0100
commita97a1ba58a2cca62093e0928b46d897865624314 (patch)
tree7ab916e27c7c40e40df33dd7f1214641554dd741
parentdcf8030739d665d203809f5bb22a6a6088fe5a7d (diff)
downloadsmtp-validate-email-a97a1ba58a2cca62093e0928b46d897865624314.zip
smtp-validate-email-a97a1ba58a2cca62093e0928b46d897865624314.tar.gz
smtp-validate-email-a97a1ba58a2cca62093e0928b46d897865624314.tar.bz2
Namespaced, unit + functional tests, PSR2, Makefile...
-rw-r--r--.editorconfig24
-rw-r--r--.gitattributes13
-rw-r--r--.gitignore4
-rw-r--r--.scrutinizer.yml41
-rw-r--r--.travis.yml51
-rw-r--r--CHANGELOG.md28
-rw-r--r--LICENSE.txt674
-rw-r--r--Makefile70
-rw-r--r--README.md154
-rw-r--r--composer.json17
-rw-r--r--package.json6
-rw-r--r--phpcs.xml.dist109
-rw-r--r--phpunit.xml.dist30
-rw-r--r--smtp-validate-email.php867
-rw-r--r--src/Exceptions/Exception.php8
-rw-r--r--src/Exceptions/NoConnection.php8
-rw-r--r--src/Exceptions/NoHelo.php8
-rw-r--r--src/Exceptions/NoMailFrom.php8
-rw-r--r--src/Exceptions/NoResponse.php8
-rw-r--r--src/Exceptions/NoTLS.php8
-rw-r--r--src/Exceptions/NoTimeout.php8
-rw-r--r--src/Exceptions/SendFailed.php8
-rw-r--r--src/Exceptions/Timeout.php8
-rw-r--r--src/Exceptions/UnexpectedResponse.php8
-rw-r--r--src/Validator.php1101
-rw-r--r--tests/Functional/ValidatorTest.php126
-rw-r--r--tests/TestCase.php25
-rw-r--r--tests/Unit/ValidatorTest.php143
28 files changed, 2663 insertions, 900 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..3e6f051
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,24 @@
+# This file is for unifying the coding style for different editors and IDEs
+# editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 4
+max_line_length = 120
+
+[{.jshintrc,*.json,*.yml}]
+indent_style = space
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
+
+# Use tabs for indentation (Makefiles require tabs)
+[{Makefile,**.mk}]
+indent_style = tab
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..396154c
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,13 @@
+# Ignore all test, docs etc with "export-ignore".
+/.editorconfig export-ignore
+/.distignore export-ignore
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/.scrutinizer.yml export-ignore
+/.styleci.yml export-ignore
+/.travis.yml export-ignore
+/phpcs.xml export-ignore
+/phpcs.xml.dist export-ignore
+/phpunit.xml export-ignore
+/phpunit.xml.dist export-ignore
+/tests export-ignore
diff --git a/.gitignore b/.gitignore
index d3bc96c..a5999bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,8 @@
nbproject
+phpunit.xml
tests.php
composer.lock
vendor/
+node_modules/
+/.phpcs.cache
+/coverage/
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
new file mode 100644
index 0000000..c97fc42
--- /dev/null
+++ b/.scrutinizer.yml
@@ -0,0 +1,41 @@
+filter:
+ paths: ['src/*']
+
+checks:
+ php: true
+
+tools:
+ external_code_coverage: true
+
+build:
+ nodes:
+ analysis:
+ environment:
+ mysql: false
+ postgresql: false
+ redis: false
+ mongodb: false
+ elasticsearch: false
+ memcached: false
+ neo4j: false
+ rabbitmq: false
+ php:
+ version: 7.1
+ cache:
+ disabled: false
+ directories:
+ - ~/.composer/cache
+ project_setup:
+ override: true
+ tests:
+ override:
+ - php-scrutinizer-run
+
+#
+#build_failure_conditions:
+# - 'elements.rating(<= C).new.exists' # No new classes/methods with a rating of C or worse allowed
+# - 'issues.label("coding-style").new.exists' # No new coding style issues allowed
+# - 'issues.severity(>= MAJOR).new.exists' # New issues of major or higher severity
+# - 'project.metric_change("scrutinizer.test_coverage", < 0)' # Code Coverage decreased from previous inspection
+# - 'patches.label("Doc Comments").new.exists' # No new doc comments patches allowed
+# - 'patches.label("Unused Use Statements").new.exists' # No new unused imports patches allowed
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..397cfd2
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,51 @@
+dist: trusty
+language: php
+sudo: false
+
+cache:
+ apt: true
+ directories:
+ - $HOME/.composer/cache/files
+
+php:
+ - 5.6
+ - 7.0
+ - 7.1
+ - 7.2
+ - nightly
+
+matrix:
+ fast_finish: true
+ allow_failures:
+ - php: nightly
+
+before_install:
+ - mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{,.disabled} || echo "xdebug not available"
+ - composer self-update
+
+install: travis_retry composer install --no-interaction
+
+script:
+ - find src tests \( -name '*.php' \) -exec php -l {} \;
+ - vendor/bin/phpunit
+
+jobs:
+ allow_failures:
+ - php: nightly
+ include:
+ - stage: Coverage
+ php: 7.1
+ before_script:
+ - mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{.disabled,}
+ - if [[ ! $(php -m | grep -si xdebug) ]]; then echo "xdebug required for coverage"; exit 1; fi
+ script:
+ - ./vendor/bin/phpunit --coverage-clover ./clover.xml
+ after_script:
+ - wget https://scrutinizer-ci.com/ocular.phar
+ - php ocular.phar code-coverage:upload --format=php-clover ./clover.xml
+ after_success:
+ - bash <(curl -s https://codecov.io/bash) -f ./clover.xml
+ - stage: CodeStyle
+ php: 7.1
+ script:
+ - ./vendor/bin/phpcs -n
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..961e35a
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,28 @@
+# Change Log
+
+## [Unreleased]
+### Added
+- n/a
+
+### Changed
+- n/a
+
+### Fixed
+- n/a
+
+## [1.0.0] - 2017-11-xx
+### Added
+- Namespaced code
+- Unit and functional tests
+- Makefile to handle development dependencies
+- Added Travis CI & Scrutinizer integration
+
+### Changed
+- Switched to PSR2 style
+
+### Fixed
+- Fixed a few edge cases (which tests revealed)
+
+## [0.6] - 2009
+### Added
+- Initial release
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..63c3050
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ {{ project }} Copyright (C) {{ year }} {{ organization }}
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..94b0197
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,70 @@
+PIDFILE=/tmp/smtp-sink.pid
+
+help: ## What you're currently reading
+ @IFS=$$'\n' ; \
+ help_lines=(`fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##/:/'`); \
+ clear ; \
+ printf "Usage: make [target]\n\n" ; \
+ printf "%-30s %s\n" "[target]" "help" ; \
+ printf "%-30s %s\n" "--------" "----" ; \
+ for help_line in $${help_lines[@]}; do \
+ IFS=$$':' ; \
+ help_split=($$help_line) ; \
+ help_command=`echo $${help_split[0]} | sed -e 's/^ *//' -e 's/ *$$//'` ; \
+ help_info=`echo $${help_split[2]} | sed -e 's/^ *//' -e 's/ *$$//'` ; \
+ printf '\033[36m'; \
+ printf "%-30s %s" $$help_command ; \
+ printf '\033[0m'; \
+ printf "%s\n" $$help_info; \
+ done; \
+ printf "\n"; \
+
+install: ## Installs dev dependencies
+ composer install
+ npm install
+
+test: server-start ## Runs tests
+ "./vendor/bin/phpunit" --no-coverage
+ make server-stop
+
+coverage: server-start ## Runs tests with code coverage
+ "./vendor/bin/phpunit" --coverage-html=./coverage/ --coverage-clover=./coverage/clover.xml
+ make server-stop
+
+$(PIDFILE): ## Starts the smtp-sink server
+ ./node_modules/.bin/smtp-sink -w allowed-sender@example.org & echo $$! > $@
+
+server-start: server-stop $(PIDFILE) ## Stops and starts the smtp-sink server
+
+server-stop: ## Stops smtp-sink server if it's running
+ if [ -e $(PIDFILE) ]; then \
+ PID=$$(cat $(PIDFILE)); \
+ # Getting smtp-sink's pid by matching second column (ppid) of ps output clumsily \
+ NODEPID=$$(ps xao pid,ppid,pgid,sid,comm | awk -v pid="$$PID" '$$2==pid{print $$1}'); \
+ #PGID=$$(ps xao pid,ppid,pgid,sid,comm -p $$PID | sed '1d' | awk '{print $$3}'); \
+ #MYPPID=$$(ps xao pid,ppid,pgid,sid,comm -p $$PID | sed '1d' | awk '{print $$2}'); \
+ echo PID=$$PID; \
+ echo NODEPID=$$NODEPID; \
+ #echo PGID=$$PGID; \
+ #echo PPID=$$PPID; \
+ #echo MYPPID=$$MYPPID; \
+ # Killing nodejs pid that we spawned (hopefully) \
+ if [ ! -z "$$NODEPID" ]; then \
+ kill $$NODEPID || true; \
+ fi; \
+ # Killing juts the PID is not enough sometimes, nodejs server still lingers on (at least on Windows) \
+ if [ ! -z "$$PID" ]; then \
+ kill $$PID || true; \
+ fi; \
+# TODO/FIXME: this manages to kill too much or something, whole recipe fails :/ \
+# if [ ! -z "$$PGID" ]; then \
+# kill -- -$$PGID || true; \
+# fi; \
+ rm -rf $(PIDFILE) || true; \
+ fi; \
+
+clean: ## Removes installed dev dependencies
+ rm -rf ./vendor
+ rm -rf ./node_modules
+
+.PHONY: help install test clean coverage server-start server-stop
diff --git a/README.md b/README.md
index 7d12f32..dc3a9aa 100644
--- a/README.md
+++ b/README.md
@@ -1,57 +1,149 @@
# SMTP\_Validate\_Email
+[![PHP Version](https://img.shields.io/badge/php-5.6%2B-blue.svg?style=flat-square)](https://packagist.org/packages/zytzagoo/smtp-validate-email)
+[![Software License](https://img.shields.io/badge/license-gpl3%2B-brightgreen.svg?style=flat-square)](LICENSE.txt)
+[![Build Status](https://img.shields.io/travis/zytzagoo/smtp-validate-email.svg?style=flat-square)](https://travis-ci.org/zytzagoo/smtp-validate-email)
+[![Scrutinizer Coverage](https://img.shields.io/scrutinizer/coverage/g/zytzagoo/smtp-validate-email.svg?style=flat-square)](https://scrutinizer-ci.com/g/zytzagoo/smtp-validate-email/?branch=master)
+
Perform email address validation/verification via SMTP.
-The class retrieves MX records for the email domain and then connects to the
+The `SMTPValidateEmail\Validator` class retrieves MX records for the email domain and then connects to the
domain's SMTP server to try figuring out if the address really exists.
-### Some features (see the source for more)
+Earlier versions (before 1.0) used the `SMTP_Validate_Email` class name (and did not use namespaces and other now-common PHP features). Care has been taken to keep the old API and migrating old code should be painless. See ["Migrating to 1.0 from older versions"](#migrating-to-1.0-from-older-versions) section. Or just use/download the ancient [0.7 version](https://github.com/zytzagoo/smtp-validate-email/releases/tag/v0.7).
-* Not really sending a message, gracefully resetting the session when done
+## Features
+* Not actually sending the message, gracefully resetting the SMTP session when done
* Command-specific communication timeouts implemented per relevant RFCs
* Catch-all account detection
* Batch mode processing supported
-* MX query support on Windows without requiring any PEAR packages
-* Logging and debugging support
+* Logging/debugging support
+* No external dependencies
+* Covered with unit/functional tests
+
+## Installation
+
+Install via [composer](https://getcomposer.org/):
+
+`composer require zytzagoo/smtp-validate-email --update-no-dev`
+
+## Usage examples
### Basic example
```php
-<?php
+require 'vendor/autoload.php';
+
+use SMTPValidateEmail\Validator as SmtpEmailValidator;
+
+/**
+ * Simple example
+ */
+$email = 'someone@example.org';
+$sender = 'sender@example.org';
+$validator = new SmtpEmailValidator($email, $sender);
+
+// If debug mode is turned on, logged data is printed as it happens:
+// $validator->debug = true;
+$results = $validator->validate();
+
+var_dump($results);
+
+// Get log data (log data is always collected)
+$log = $validator->getLog();
+var_dump($log);
+```
+
+### Multiple recipients and other details
+
+```php
+require 'vendor/autoload.php';
+
+use SMTPValidateEmail\Validator as SmtpEmailValidator;
-require('smtp-validate-email.php');
+/**
+ * Validating multiple addresses/recipients at once:
+ * (checking multiple addresses belonging to the same server
+ * uses a single connection)
+ */
+$emails = [
+ 'someone@example.org',
+ 'someone.else@example.com'
+];
+$sender = 'sender@example.org';
+$validator = new SmtpEmailValidator($email, $sender):
+$results = $validator->validate();
-$from = 'a-happy-camper@campspot.net'; // for SMTP FROM:<> command
-$email = 'someone@somewhere.net';
+var_dump($results);
-$validator = new SMTP_Validate_Email($email, $from);
-$smtp_results = $validator->validate();
+/**
+ * The `validate()` method accepts the same parameters
+ * as the constructor, so this is equivalent to the above:
+ */
+$emails = [
+ 'someone@example.org',
+ 'someone.else@example.com'
+];
+$sender = 'sender@example.org';
+$validator = new SmtpEmailValidator():
+$results = $validator->validate($emails, $sender);
-var_dump($smtp_results);
+var_dump($results);
```
-### Array usage
-The class supports passing an array of addresses in the constructor or to the
-`validate()` method. Checking multiple addresses on the same server uses
-a single connection.
+## Migrating to 1.0 from older versions
+
+Earlier versions used the global `SMTP_Validate_Email` classname.
+You can keep using that name in your existing code and still switch to the newer (composer-powered) version by using [aliasing/importing](http://php.net/manual/en/language.namespaces.importing.php) like this:
+
+Require the composer package:
+
+`composer require zytzagoo/smtp-validate-email --update-no-dev`
+
+And then in your code:
+
```php
-<?php
+require 'vendor/autoload.php';
+
+use SMTPValidateEmail\Validator as SMTP_Validate_Email;
-require('smtp-validate-email.php');
+// Now any old code referencing `SMTP_Validate_Email` should still work as it did earlier
+```
-$from = 'a-happy-camper@campspot.net'; // for SMTP FROM:<> command
-$emails = array(
- 'someone@somewhere.net',
- 'some-other@somewhere-else.net',
- 'someone@example.com',
- 'someone-else@example.com'
-);
+## Development & Contributions
+See the [Makefile](Makefile) and the development dependencies in [composer.json](composer.json) and [package.json](package.json).
-$validator = new SMTP_Validate_Email($emails, $from);
-$smtp_results = $validator->validate();
+Running `make` once you clone (or download) the repository gives you:
-// or passing to the validate() method
-// $validator = new SMTP_Validate_Email();
-// $smtp_results = $validator->validate($emails, $from);
+```
+Usage: make [target]
-var_dump($smtp_results);
+[target] help
+-------- ----
+help What you're currently reading
+install Installs dev dependencies
+test Runs tests
+coverage Runs tests with code coverage
+$(PIDFILE) Starts the smtp-sink server
+server-start Stops and starts the smtp-sink server
+server-stop Stops smtp-sink server if it's running
+clean Removes installed dev dependencies
```
+
+So, run `make install` to get started. Afterwards you should be able to run the tests.
+
+Tests are powered by `phpunit` and a local `smtp-sink` instance running on port 1025.
+If `smtp-sink` is unavailable, tests requiring it are marked as skipped.
+
+Pull requests are welcome!
+
+In order to get your pull-request merged,
+please follow these simple rules:
+
+* all code submissions must pass cleanly (no errors) with `make test`
+* stick to existing code style (`phpcs` is used)
+* there should be no external dependencies
+* if you want to add significant features/dependencies, file an issue about it first so we can discuss whether the addition makes sense for the project
+
+## [Changelog](CHANGELOG.md)
+
+## [License (GPL-3.0+)](LICENSE.txt)
diff --git a/composer.json b/composer.json
index 7b7fd72..0829dbb 100644
--- a/composer.json
+++ b/composer.json
@@ -22,9 +22,22 @@
}
],
"require": {
- "php": ">=5.3.1"
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.4.2",
+ "phpunit/phpunit": "~4.8|~5.1",
+ "squizlabs/php_codesniffer": "^3.0",
+ "wimg/php-compatibility": "^8.0"
},
"autoload": {
- "classmap": ["smtp-validate-email.php"]
+ "psr-4": {
+ "SMTPValidateEmail\\": "src"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "SMTPValidateEmail\\Tests\\": "tests"
+ }
}
}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f8b6b2c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,6 @@
+{
+ "private": true,
+ "devDependencies":{
+ "smtp-sink":"0.0.3"
+ }
+}
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
new file mode 100644
index 0000000..2a5993c
--- /dev/null
+++ b/phpcs.xml.dist
@@ -0,0 +1,109 @@
+<?xml version="1.0"?>
+<ruleset>
+ <arg name="cache" value=".phpcs.cache"/>
+ <file>./src</file>
+ <file>./tests</file>
+ <exclude-pattern>./coverage</exclude-pattern>
+
+ <!-- Show sniff codes in all reports -->
+ <arg value="sp"/>
+
+ <!-- Check for cross-version support for PHP -->
+ <config name="testVersion" value="5.6"/>
+ <rule ref="PHPCompatibility"/>
+
+ <!-- PSR2 + some additions -->
+ <rule ref="PSR2"/>
+
+ <rule ref="Generic.Arrays.DisallowLongArraySyntax"/>
+ <rule ref="Generic.Classes.DuplicateClassName"/>
+ <rule ref="Generic.CodeAnalysis.EmptyStatement"/>
+ <rule ref="Generic.CodeAnalysis.ForLoopShouldBeWhileLoop"/>
+ <rule ref="Generic.CodeAnalysis.ForLoopWithTestFunctionCall"/>
+ <rule ref="Generic.CodeAnalysis.JumbledIncrementer"/>
+ <rule ref="Generic.CodeAnalysis.UnnecessaryFinalModifier"/>
+ <rule ref="Generic.CodeAnalysis.UnusedFunctionParameter"/>
+ <rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/>
+ <rule ref="Generic.ControlStructures.InlineControlStructure"/>
+ <rule ref="Generic.Files.ByteOrderMark"/>
+ <rule ref="Generic.Files.LineEndings"/>
+ <rule ref="Generic.Formatting.DisallowMultipleStatements"/>
+ <rule ref="Generic.Formatting.SpaceAfterCast"/>
+ <rule ref="Generic.Formatting.MultipleStatementAlignment">
+ <properties>
+ <property name="ignoreMultiLine" value="true"/>
+ </properties>
+ </rule>
+ <rule ref="Generic.Functions.CallTimePassByReference"/>
+ <rule ref="Generic.NamingConventions.ConstructorName"/>
+ <rule ref="Generic.NamingConventions.UpperCaseConstantName"/>
+ <rule ref="Generic.PHP.DeprecatedFunctions"/>
+ <rule ref="Generic.PHP.DisallowShortOpenTag"/>
+ <rule ref="Generic.PHP.ForbiddenFunctions"/>
+ <rule ref="Generic.PHP.LowerCaseConstant"/>
+ <rule ref="Generic.PHP.NoSilencedErrors"/>
+ <rule ref="Generic.PHP.Syntax"/>
+ <rule ref="Generic.Strings.UnnecessaryStringConcat"/>
+ <rule ref="Generic.WhiteSpace.DisallowTabIndent"/>
+ <rule ref="Generic.WhiteSpace.ScopeIndent">
+ <properties>
+ <property name="exact" value="false"/>
+ </properties>
+ </rule>
+ <rule ref="PEAR.WhiteSpace.ScopeClosingBrace"/>
+ <rule ref="PEAR.WhiteSpace.ObjectOperatorIndent"/>
+ <rule ref="PEAR.Classes.ClassDeclaration"/>
+ <rule ref="Squiz.Commenting.DocCommentAlignment"/>
+ <rule ref="Squiz.Classes.SelfMemberReference"/>
+ <rule ref="Squiz.Classes.ClassFileName"/>
+ <rule ref="Squiz.Classes.ClassDeclaration"/>
+ <rule ref="Squiz.Commenting.EmptyCatchComment"/>
+ <rule ref="Squiz.Operators.IncrementDecrementUsage"/>
+ <rule ref="Squiz.Functions.MultiLineFunctionDeclaration"/>
+ <rule ref="Squiz.Functions.LowercaseFunctionKeywords"/>
+ <rule ref="Squiz.Functions.FunctionDeclaration"/>
+ <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing"/>
+ <rule ref="Squiz.Formatting.OperatorBracket"/>
+ <rule ref="Squiz.PHP.DisallowObEndFlush"/>
+ <rule ref="Squiz.PHP.DisallowSizeFunctionsInLoops"/>
+ <rule ref="Squiz.PHP.DisallowMultipleAssignments"/>
+ <rule ref="Squiz.PHP.DiscouragedFunctions">
+ <severity>0</severity>
+ </rule>
+ <rule ref="Squiz.PHP.EmbeddedPhp"/>
+ <rule ref="Squiz.PHP.Eval"/>
+ <rule ref="Squiz.PHP.ForbiddenFunctions"/>
+ <rule ref="Squiz.PHP.InnerFunctions"/>
+ <rule ref="Squiz.PHP.LowercasePHPFunctions"/>
+ <rule ref="Squiz.PHP.NonExecutableCode"/>
+ <rule ref="Squiz.Scope.MemberVarScope"/>
+ <rule ref="Squiz.Scope.MethodScope"/>
+ <rule ref="Squiz.Scope.StaticThisUsage"/>
+ <rule ref="Squiz.Strings.DoubleQuoteUsage"/>
+ <rule ref="Squiz.WhiteSpace.CastSpacing"/>
+ <rule ref="Squiz.WhiteSpace.ControlStructureSpacing"/>
+ <rule ref="Squiz.WhiteSpace.LanguageConstructSpacing"/>
+ <rule ref="Squiz.WhiteSpace.LogicalOperatorSpacing"/>
+ <rule ref="Squiz.WhiteSpace.MemberVarSpacing"/>
+ <rule ref="Squiz.WhiteSpace.OperatorSpacing"/>
+ <rule ref="Squiz.WhiteSpace.PropertyLabelSpacing"/>
+ <rule ref="Squiz.WhiteSpace.ScopeClosingBrace"/>
+ <rule ref="Squiz.WhiteSpace.ScopeKeywordSpacing"/>
+ <rule ref="Squiz.WhiteSpace.SuperfluousWhitespace">
+ <properties>
+ <property name="ignoreBlankLines" value="true"/>
+ </properties>
+ </rule>
+ <rule ref="Squiz.WhiteSpace.SuperfluousWhitespace.StartFile">
+ <severity>0</severity>
+ </rule>
+ <rule ref="Squiz.WhiteSpace.SuperfluousWhitespace.EndFile">
+ <severity>0</severity>
+ </rule>
+ <rule ref="Squiz.WhiteSpace.SuperfluousWhitespace.EmptyLines">
+ <severity>0</severity>
+ </rule>
+ <rule ref="Squiz.WhiteSpace.SemicolonSpacing"/>
+ <rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"/>
+ <rule ref="Zend.Files.ClosingTag"/>
+</ruleset>
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..1fb775c
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.8/phpunit.xsd"
+ backupGlobals="false"
+ backupStaticAttributes="false"
+ bootstrap="vendor/autoload.php"
+ colors="true"
+ convertErrorsToExceptions="true"
+ convertNoticesToExceptions="true"
+ convertWarningsToExceptions="true"
+ processIsolation="false"
+ stopOnFailure="false">
+ <testsuites>
+ <testsuite name="unit">
+ <directory suffix="Test.php">tests/Unit</directory>
+ </testsuite>
+ <testsuite name="functional">
+ <directory suffix="Test.php">tests/Functional</directory>
+ </testsuite>
+ </testsuites>
+ <filter>
+ <whitelist addUncoveredFilesFromWhitelist="true">
+ <directory suffix=".php">src</directory>
+ </whitelist>
+ <blacklist>
+ <directory suffix=".php">vendor</directory>
+ </blacklist>
+ </filter>
+</phpunit>
diff --git a/smtp-validate-email.php b/smtp-validate-email.php
deleted file mode 100644
index 94df305..0000000
--- a/smtp-validate-email.php
+++ /dev/null
@@ -1,867 +0,0 @@
-<?php
-/**
-* SMTP_Validate_Email - Perform email address verification via SMTP.
-* Copyright (C) 2009 Tomaš Trkulja [zytzagoo] <zyt@zytzagoo.net>
-*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU 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 General Public License for more details.
-*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see <http://www.gnu.org/licenses/>.
-*
-* @version 0.7
-* @todo
-* - finish the graylisting thingy
-* - perhaps re-implement some methods as static?
-* - introduce a main socket loop if this state-based approach doesn't work out
-* - implement TLS probably
-* - more code examples, more tests
-*
-* The class retrieves MX records for the email domain and then connects to the
-* domain's SMTP server to try figuring out if the address is really valid.
-*
-* Some ideas taken from: http://code.google.com/p/php-smtp-email-validation
-* See the source and comments for more details.
-*/
-
-// Exceptions we throw
-class SMTP_Validate_Email_Exception extends Exception {}
-class SMTP_Validate_Email_Exception_Timeout extends SMTP_Validate_Email_Exception {}
-class SMTP_Validate_Email_Exception_Unexpected_Response extends SMTP_Validate_Email_Exception {}
-class SMTP_Validate_Email_Exception_No_Response extends SMTP_Validate_Email_Exception {}
-class SMTP_Validate_Email_Exception_No_Connection extends SMTP_Validate_Email_Exception {}
-class SMTP_Validate_Email_Exception_No_Helo extends SMTP_Validate_Email_Exception {}
-class SMTP_Validate_Email_Exception_No_Mail_From extends SMTP_Validate_Email_Exception {}
-class SMTP_Validate_Email_Exception_No_Timeout extends SMTP_Validate_Email_Exception {}
-class SMTP_Validate_Email_Exception_No_TLS extends SMTP_Validate_Email_Exception {}
-class SMTP_Validate_Email_Exception_Send_Failed extends SMTP_Validate_Email_Exception {}
-
-// SMTP validation class
-class SMTP_Validate_Email {
-
- // holds the socket connection resource
- private $socket;
-
- // holds all the domains we'll validate accounts on
- private $domains;
-
- private $domains_info = array();
-
- // connect timeout for each MTA attempted (seconds)
- private $connect_timeout = 10;
-
- // default username of sender
- private $from_user = 'user';
-
- // default host of sender
- private $from_domain = 'localhost';
-
- // the host we're currently connected to
- private $host = null;
-
- // holds all the debug info
- public $log = array();
-
- // array of validation results
- private $results = array();
-
- // states we can be in
- private $state = array(
- 'helo' => false,
- 'mail' => false,
- 'rcpt' => false
- );
-
- // print stuff as it happens or not
- public $debug = false;
-
- // default smtp port
- public $connect_port = 25;
-
- /**
- * Are 'catch-all' accounts considered valid or not?
- * If not, the class checks for a "catch-all" and if it determines the box
- * has a "catch-all", sets all the emails on that domain as invalid.
- */
- public $catchall_is_valid = true;
- public $catchall_test = false; // Set to true to perform a catchall test
-
- /**
- * Being unable to communicate with the remote MTA could mean an address
- * is invalid, but it might not, depending on your use case, set the
- * value appropriately.
- */
- public $no_comm_is_valid = false;
-
- /**
- * Being unable to connect with the remote host could mean a server
- * configuration issue, but it might not, depending on your use case,
- * set the value appropriately.
- */
- public $no_conn_is_valid = false;
-
- // do we consider "greylisted" responses as valid or invalid addresses
- public $greylisted_considered_valid = true;
-
- /**
- * If on Windows (or other places that don't have getmxrr()), this is the
- * nameserver that will be used for MX querying.
- * Set as empty to use the DNS specified via your current network connection.
- * @see getmxrr()
- */
- // protected $mx_query_ns = 'dns1.t-com.hr';
- protected $mx_query_ns = '';
-
- /**
- * Timeout values for various commands (in seconds) per RFC 2821
- * @see expect()
- */
- protected $command_timeouts = array(
- 'ehlo' => 120,
- 'helo' => 120,
- 'tls' => 180, // start tls
- 'mail' => 300, // mail from
- 'rcpt' => 300, // rcpt to,
- 'rset' => 30,
- 'quit' => 60,
- 'noop' => 60
- );
-
- // some constants
- const CRLF = "\r\n";
-
- // some smtp response codes
- const SMTP_CONNECT_SUCCESS = 220;
- const SMTP_QUIT_SUCCESS = 221;
- const SMTP_GENERIC_SUCCESS = 250;
- const SMTP_USER_NOT_LOCAL = 251;
- const SMTP_CANNOT_VRFY = 252;
-
- const SMTP_SERVICE_UNAVAILABLE = 421;
-
- // 450 Requested mail action not taken: mailbox unavailable (e.g.,
- // mailbox busy or temporarily blocked for policy reasons)
- const SMTP_MAIL_ACTION_NOT_TAKEN = 450;
- // 451 Requested action aborted: local error in processing
- const SMTP_MAIL_ACTION_ABORTED = 451;
- // 452 Requested action not taken: insufficient system storage
- const SMTP_REQUESTED_ACTION_NOT_TAKEN = 452;
-
- // 500 Syntax error (may be due to a denied command)
- const SMTP_SYNTAX_ERROR = 500;
- // 502 Comment not implemented
- const SMTP_NOT_IMPLEMENTED = 502;
- // 503 Bad sequence of commands (may be due to a denied command)
- const SMTP_BAD_SEQUENCE = 503;
-
- // 550 Requested action not taken: mailbox unavailable (e.g., mailbox
- // not found, no access, or command rejected for policy reasons)
- const SMTP_MBOX_UNAVAILABLE = 550;
-
- // 554 Seen this from hotmail MTAs, in response to RSET :(
- const SMTP_TRANSACTION_FAILED = 554;
-
- // list of codes considered as "greylisted"
- private $greylisted = array(
- self::SMTP_MAIL_ACTION_NOT_TAKEN,
- self::SMTP_MAIL_ACTION_ABORTED,
- self::SMTP_REQUESTED_ACTION_NOT_TAKEN
- );
-
- /**
- * Constructor.
- * @param $emails array [optional] Array of emails to validate
- * @param $sender string [optional] Email address of the sender/validator
- */
- function __construct($emails = array(), $sender = '') {
- if (!empty($emails)) {
- $this->set_emails($emails);
- }
- if (!empty($sender)) {
- $this->set_sender($sender);
- }
- }
-
- /**
- * Disconnects from the SMTP server if needed.
- * @return void
- */
- public function __destruct() {
- $this->disconnect(false);
- }
-
- public function accepts_any_recipient($domain) {
- if (!$this->catchall_test) {
- return false;
- }
- $test = 'catch-all-test-' . time();
- $accepted = $this->rcpt($test . '@' . $domain);
- if ($accepted) {
- // success on a non-existing address is a "catch-all"
- $this->domains_info[$domain]['catchall'] = true;
- return true;
- }
- // log the case in which we get disconnected
- // while trying to perform a catchall detect
- $this->noop();
- if (!($this->connected())) {
- $this->debug('Disconnected after trying a non-existing recipient on ' . $domain);
- }
- // nb: disconnects are considered as a non-catch-all case this way
- // this might not be true always
- return false;
- }
-
- /**
- * Performs validation of specified email addresses.
- * @param array $emails Emails to validate (recipient emails)
- * @param string $sender Sender email address
- * @return array List of emails and their results
- */
- public function validate($emails = array(), $sender = '') {
-
- $this->results = array();
-
- if (!empty($emails)) {
- $this->set_emails($emails);
- }
- if (!empty($sender)) {
- $this->set_sender($sender);
- }
-
- if (!is_array($this->domains) || empty($this->domains)) {
- return $this->results;
- }
-
- // query the MTAs on each domain if we have them
- foreach ($this->domains as $domain => $users) {
-
- $mxs = array();
-
- // query the mx records for the current domain
- list($hosts, $weights) = $this->mx_query($domain);
-
- // sort out the MX priorities
- foreach ($hosts as $k => $host) {
- $mxs[$host] = $weights[$k];
- }
- asort($mxs);
-
- // add the hostname itself with 0 weight (RFC 2821)
- $mxs[$domain] = 0;
-
- $this->debug('MX records (' . $domain . '): ' . print_r($mxs, true));
- $this->domains_info[$domain] = array();
- $this->domains_info[$domain]['users'] = $users;
- $this->domains_info[$domain]['mxs'] = $mxs;
-
- // try each host, $_weight unused in the foreach body, but array_keys() doesn't guarantee the order
- foreach ($mxs as $host => $_weight) {
- // try connecting to the remote host
- try {
- $this->connect($host);
- if ($this->connected()) {
- break;
- }
- } catch (SMTP_Validate_Email_Exception_No_Connection $e) {
- // unable to connect to host, so these addresses are invalid?
- $this->debug('Unable to connect. Exception caught: ' . $e->getMessage());
- $this->set_domain_results($users, $domain, $this->no_conn_is_valid );
- }
- }
-
- // are we connected?
- if ($this->connected()) {
- try {
- // say helo, and continue if we can talk
- if ($this->helo()) {
-
- // try issuing MAIL FROM
- if (!($this->mail($this->from_user . '@' . $this->from_domain))) {
- // MAIL FROM not accepted, we can't talk
- $this->set_domain_results($users, $domain, $this->no_comm_is_valid);
- }
-
- /**
- * if we're still connected, proceed (cause we might get
- * disconnected, or banned, or greylisted temporarily etc.)
- * see mail() for more
- */
- if ($this->connected()) {
-
- $this->noop();
-
- // attempt a catch-all test for the domain (if configured to do so)
- $is_catchall_domain = $this->accepts_any_recipient($domain);
-
- // if a catchall domain is detected, and we consider
- // accounts on such domains as invalid, mark all the
- // users as invalid and move on
- if ($is_catchall_domain) {
- if (!($this->catchall_is_valid)) {
- $this->set_domain_results($users, $domain, $this->catchall_is_valid);
- continue;
- }
- }
-
- // if we're still connected, try issuing rcpts
- if ($this->connected()) {
- $this->noop();
- // rcpt to for each user
- foreach ($users as $user) {
- $address = $user . '@' . $domain;
- $this->results[$address] = $this->rcpt($address);
- $this->noop();
- }
- }
-
- // saying buh-bye if we're still connected, cause we're done here
- if ($this->connected()) {
- // issue a rset for all the things we just made the MTA do
- $this->rset();
- // kiss it goodbye
- $this->disconnect();
- }
-
- }
-
- } else {
-
- // we didn't get a good response to helo and should be disconnected already
- $this->set_domain_results($users, $domain, $this->no_comm_is_valid);
-
- }
-
- } catch (SMTP_Validate_Email_Exception_Unexpected_Response $e) {
-
- // Unexpected responses handled as $this->no_comm_is_valid, that way anyone can
- // decide for themselves if such results are considered valid or not
- $this->set_domain_results($users, $domain, $this->no_comm_is_valid);
-
- } catch (SMTP_Validate_Email_Exception_Timeout $e) {
-
- // A timeout is a comm failure, so treat the results on that domain
- // according to $this->no_comm_is_valid as well
- $this->set_domain_results($users, $domain, $this->no_comm_is_valid);
-
- }
- }
-
- }
-
- return $this->get_results();
-
- }
-
- public function get_results($include_domains_info = true) {
- if ($include_domains_info) {
- $this->results['domains'] = $this->domains_info;
- }
- return $this->results;
- }
-
- /**
- * Helper to set results for all the users on a domain to a specific value
- * @param array $users Array of users (usernames)
- * @param string $domain The domain
- * @param bool $val Value to set
- */
- private function set_domain_results($users, $domain, $val) {
- if (!is_array($users)) {
- $users = (array) $users;
- }
- foreach ($users as $user) {
- $this->results[$user . '@' . $domain] = $val;
- }
- }
-
- /**
- * Returns true if we're connected to an MTA
- * @return bool
- */
- protected function connected() {
- return is_resource($this->socket);
- }
-
- /**
- * Tries to connect to the specified host on the pre-configured port.
- * @param string $host The host to connect to
- * @return void
- * @throws SMTP_Validate_Email_Exception_No_Connection
- * @throws SMTP_Validate_Email_Exception_No_Timeout
- */
- protected function connect($host) {
- $remote_socket = $host . ':' . $this->connect_port;
- $errnum = 0;
- $errstr = '';
- $this->host = $remote_socket;
- // open connection
- $this->debug('Connecting to ' . $this->host);
- $this->socket = @stream_socket_client(
- $this->host,
- $errnum,
- $errstr,
- $this->connect_timeout,
- STREAM_CLIENT_CONNECT,
- stream_context_create(array())
- );
- // connected?
- if (!$this->connected()) {
- $this->debug('Connect failed: ' . $errstr . ', error number: ' . $errnum . ', host: ' . $this->host);
- throw new SMTP_Validate_Email_Exception_No_Connection('Cannot ' .
- 'open a connection to remote host (' . $this->host . ')');
- }
- $result = stream_set_timeout($this->socket, $this->connect_timeout);
- if (!$result) {
- throw new SMTP_Validate_Email_Exception_No_Timeout('Cannot set timeout');
- }
- $this->debug('Connected to ' . $this->host . ' successfully');
- }
-
- /**
- * Disconnects the currently connected MTA.
- * @param bool $quit Issue QUIT before closing the socket on our end.
- * @return void
- */
- protected function disconnect($quit = true) {
- if ($quit) {
- $this->quit();
- }
- if ($this->connected()) {
- $this->debug('Closing socket to ' . $this->host);
- fclose($this->socket);
- }
- $this->host = null;
- $this->reset_state();
- }
-
- /**
- * Resets internal state flags to defaults
- */
- private function reset_state() {
- $this->state['helo'] = false;
- $this->state['mail'] = false;
- $this->state['rcpt'] = false;
- }
-
- /**
- * Sends a HELO/EHLO sequence
- * @todo Implement TLS
- * @return bool|null True if successful, false otherwise
- */
- protected function helo() {
- // don't try if it was already done
- if ($this->state['helo']) {
- return null;
- }
- try {
- $this->expect(self::SMTP_CONNECT_SUCCESS, $this->command_timeouts['helo']);
- $this->ehlo();
- // session started
- $this->state['helo'] = true;
- // are we going for a TLS connection?
- /*
- if ($this->tls == true) {
- // send STARTTLS, wait 3 minutes
- $this->send('STARTTLS');
- $this->expect(self::SMTP_CONNECT_SUCCESS, $this->command_timeouts['tls']);
- $result = stream_socket_enable_crypto($this->socket, true,
- STREAM_CRYPTO_METHOD_TLS_CLIENT);
- if (!$result) {
- throw new SMTP_Validate_Email_Exception_No_TLS('Cannot enable TLS');
- }
- }
- */
- return true;
- } catch (SMTP_Validate_Email_Exception_Unexpected_Response $e) {
- // connected, but recieved an unexpected response, so disconnect
- $this->debug('Unexpected response after connecting: ' . $e->getMessage());
- $this->disconnect(false);
- return false;
- }
- }
-
- /**
- * Send EHLO or HELO, depending on what's supported by the remote host.
- * @return void
- */
- protected function ehlo() {
- try {
- // modern
- $this->send('EHLO ' . $this->from_domain);
- $this->expect(self::SMTP_GENERIC_SUCCESS, $this->command_timeouts['ehlo']);
- } catch (SMTP_Validate_Email_Exception_Unexpected_Response $e) {
- // legacy
- $this->send('HELO ' . $this->from_domain);
- $this->expect(self::SMTP_GENERIC_SUCCESS, $this->command_timeouts['helo']);
- }
- }
-
- /**
- * Sends a MAIL FROM command to indicate the sender.
- * @param string $from The "From:" address
- * @return bool If MAIL FROM command was accepted or not
- * @throws SMTP_Validate_Email_Exception_No_Helo
- */
- protected function mail($from) {
- if (!$this->state['helo']) {
- throw new SMTP_Validate_Email_Exception_No_Helo('Need HELO before MAIL FROM');
- }
- // issue MAIL FROM, 5 minute timeout
- $this->send('MAIL FROM:<' . $from . '>');
- try {
- $this->expect(self::SMTP_GENERIC_SUCCESS, $this->command_timeouts['mail']);
- // set state flags
- $this->state['mail'] = true;
- $this->state['rcpt'] = false;
- return true;
- } catch (SMTP_Validate_Email_Exception_Unexpected_Response $e) {
- // got something unexpected in response to MAIL FROM
- $this->debug("Unexpected response to MAIL FROM\n:" . $e->getMessage());
- // hotmail has been known to do this + was closing the connection
- // forcibly on their end, so we're killing the socket here too
- $this->disconnect(false);
- return false;
- }
- }
-
- /**
- * Sends a RCPT TO command to indicate a recipient.
- * @param string $to Recipient's email address
- * @return bool Is the recipient accepted
- * @throws SMTP_Validate_Email_Exception_No_Mail_From
- */
- protected function rcpt($to) {
- // need to have issued MAIL FROM first
- if (!$this->state['mail']) {
- throw new SMTP_Validate_Email_Exception_No_Mail_From('Need MAIL FROM before RCPT TO');
- }
- $is_valid = false;
- $expected_codes = array(
- self::SMTP_GENERIC_SUCCESS,
- self::SMTP_USER_NOT_LOCAL
- );
- if ($this->greylisted_considered_valid) {
- $expected_codes = array_merge($expected_codes, $this->greylisted);
- }
- // issue RCPT TO, 5 minute timeout
- try {
- $this->send('RCPT TO:<' . $to . '>');
- // process the response
- try {
- $this->expect($expected_codes, $this->command_timeouts['rcpt']);
- $this->state['rcpt'] = true;
- $is_valid = true;
- } catch (SMTP_Validate_Email_Exception_Unexpected_Response $e) {
- $this->debug('Unexpected response to RCPT TO: ' . $e->getMessage());
- }
- } catch (SMTP_Validate_Email_Exception $e) {
- $this->debug('Sending RCPT TO failed: ' . $e->getMessage());
- }
- return $is_valid;
- }
-
- /**
- * Sends a RSET command and resets our internal state.
- * @return void
- */
- protected function rset() {
- $this->send('RSET');
- // MS ESMTP doesn't follow RFC according to ZF tracker, see [ZF-1377]
- $expected = array(
- self::SMTP_GENERIC_SUCCESS,
- self::SMTP_CONNECT_SUCCESS,
- self::SMTP_NOT_IMPLEMENTED,
- // hotmail returns this o_O
- self::SMTP_TRANSACTION_FAILED
- );
- $this->expect($expected, $this->command_timeouts['rset'], true);
- $this->state['mail'] = false;
- $this->state['rcpt'] = false;
- }
-
- /**
- * Sends a QUIT command.
- * @return void
- */
- protected function quit() {
- // although RFC says QUIT can be issued at any time, we won't
- if ($this->state['helo']) {
- $this->send('QUIT');
- $this->expect(array(self::SMTP_GENERIC_SUCCESS,self::SMTP_QUIT_SUCCESS), $this->command_timeouts['quit'], true);
- }
- }
-
- /**
- * Sends a NOOP command.
- * @return void
- */
- protected function noop() {
- $this->send('NOOP');
- // erg... "SMTP" code fix some bad RFC implementations
- // Found at least 1 SMTP server replying to NOOP without
- // any SMTP code.
- $expected_codes = array(
- 'SMTP',
- self::SMTP_BAD_SEQUENCE,
- self::SMTP_NOT_IMPLEMENTED,
- self::SMTP_GENERIC_SUCCESS,
- self::SMTP_SYNTAX_ERROR,
- self::SMTP_CONNECT_SUCCESS
- );
- $this->expect($expected_codes, $this->command_timeouts['noop'], true);
- }
-
- /**
- * Sends a command to the remote host.
- * @param string $cmd The cmd to send
- * @return int|bool Number of bytes written to the stream
- * @throws SMTP_Validate_Email_Exception_No_Connection
- * @throws SMTP_Validate_Email_Exception_Send_Failed
- */
- protected function send($cmd) {
- // must be connected
- if (!$this->connected()) {
- throw new SMTP_Validate_Email_Exception_No_Connection('No connection');
- }
- $this->debug('send>>>: ' . $cmd);
- // write the cmd to the connection stream
- $result = fwrite($this->socket, $cmd . self::CRLF);
- // did the send work?
- if ($result === false) {
- throw new SMTP_Validate_Email_Exception_Send_Failed('Send failed ' .
- 'on: ' . $this->host);
- }
- return $result;
- }
-
- /**
- * Receives a response line from the remote host.
- * @param int $timeout Timeout in seconds
- * @return string
- * @throws SMTP_Validate_Email_Exception_No_Connection
- * @throws SMTP_Validate_Email_Exception_Timeout
- * @throws SMTP_Validate_Email_Exception_No_Response
- */
- protected function recv($timeout = null) {
- if (!$this->connected()) {
- throw new SMTP_Validate_Email_Exception_No_Connection('No connection');
- }
- // timeout specified?
- if ($timeout !== null) {
- stream_set_timeout($this->socket, $timeout);
- }
- // retrieve response
- $line = fgets($this->socket, 1024);
- $this->debug('<<<recv: ' . $line);
- // have we timed out?
- $info = stream_get_meta_data($this->socket);
- if (!empty($info['timed_out'])) {
- throw new SMTP_Validate_Email_Exception_Timeout('Timed out in recv');
- }
- // did we actually receive anything?
- if ($line === false) {
- throw new SMTP_Validate_Email_Exception_No_Response('No response in recv');
- }
- return $line;
- }
-
- /**
- * Receives lines from the remote host and looks for expected response codes.
- * @param int|int[] $codes A list of one or more expected response codes
- * @param int $timeout The timeout for this individual command, if any
- * @param bool $empty_response_allowed When true, empty responses are allowed
- * @return string The last text message received
- * @throws SMTP_Validate_Email_Exception_Unexpected_Response
- */
- protected function expect($codes, $timeout = null, $empty_response_allowed = false) {
- if (!is_array($codes)) {
- $codes = (array) $codes;
- }
- $code = null;
- $text = '';
- try {
-
- $text = $line = $this->recv($timeout);
- while (preg_match("/^[0-9]+-/", $line)) {
- $line = $this->recv($timeout);
- $text .= $line;
- }
- sscanf($line, '%d%s', $code, $text);
- if (($empty_response_allowed === false && ($code === null || !in_array($code, $codes))) || $code == self::SMTP_SERVICE_UNAVAILABLE) {
- throw new SMTP_Validate_Email_Exception_Unexpected_Response($line);
- }
-
- } catch (SMTP_Validate_Email_Exception_No_Response $e) {
-
- // no response in expect() probably means that the
- // remote server forcibly closed the connection so
- // lets clean up on our end as well?
- $this->debug('No response in expect(): ' . $e->getMessage());
- $this->disconnect(false);
-
- }
- return $text;
- }
-
- /**
- * Parses an email string into respective user and domain parts and
- * returns those as an array.
- * @param string $email 'user@domain'
- * @return array ['user', 'domain']
- */
- protected function parse_email($email) {
- $parts = explode('@', $email);
- $domain = array_pop($parts);
- $user= implode('@', $parts);
- return array($user, $domain);
- }
-
- /**
- * Sets the email addresses that should be validated.
- * @param array $emails Array of emails to validate
- * @return void
- */
- public function set_emails($emails) {
- if (!is_array($emails)) {
- $emails = (array) $emails;
- }
- $this->domains = array();
- foreach ($emails as $email) {
- list($user, $domain) = $this->parse_email($email);
- if (!isset($this->domains[$domain])) {
- $this->domains[$domain] = array();
- }
- $this->domains[$domain][] = $user;
- }
- }
-
- /**
- * Sets the email address to use as the sender/validator.
- * @param string $email
- * @return void
- */
- public function set_sender($email) {
- $parts = $this->parse_email($email);
- $this->from_user = $parts[0];
- $this->from_domain = $parts[1];
- }
-
- /**
- * Queries the DNS server for MX entries of a certain domain.
- * @param string $domain The domain for which to retrieve MX records
- * @return array MX hosts and their weights
- */
- protected function mx_query($domain) {
- $hosts = array();
- $weight = array();
- if (function_exists('getmxrr')) {
- getmxrr($domain, $hosts, $weight);
- } else {
- $this->getmxrr($domain, $hosts, $weight);
- }
- return array($hosts, $weight);
- }
-
- /**
- * Provides a windows replacement for the getmxrr function.
- * Params and behaviour is that of the regular getmxrr function.
- * @see http://www.php.net/getmxrr
- * @param string $hostname
- * @param string[] $mxhosts
- * @param int[] $mxweights
- * @return bool|null
- */
- protected function getmxrr($hostname, &$mxhosts, &$mxweights) {
- if (!is_array($mxhosts)) {
- $mxhosts = array();
- }
- if (!is_array($mxweights)) {
- $mxweights = array();
- }
- if (empty($hostname)) {
- return null;
- }
- $cmd = 'nslookup -type=MX ' . escapeshellarg($hostname);
- if (!empty($this->mx_query_ns)) {
- $cmd .= ' ' . escapeshellarg($this->mx_query_ns);
- }
- exec($cmd, $output);
- if (empty($output)) {
- return null;
- }
- $i = -1;
- foreach ($output as $line) {
- $i++;
- if (preg_match("/^$hostname\tMX preference = ([0-9]+), mail exchanger = (.+)$/i", $line, $parts)) {
- $mxweights[$i] = trim($parts[1]);
- $mxhosts[$i] = trim($parts[2]);
- }
- if (preg_match('/responsible mail addr = (.+)$/i', $line, $parts)) {
- $mxweights[$i] = $i;
- $mxhosts[$i] = trim($parts[1]);
- }
- }
- return ($i != -1);
- }
-
- /**
- * Debug helper. If run in a CLI env, it just dumps $str on a new line,
- * else it prints stuff using <pre>.
- * @param string $str The debug message
- * @return void
- */
- private function debug($str) {
- $str = $this->stamp($str);
- $this->log($str);
- if ($this->debug == true) {
- if (PHP_SAPI != 'cli') {
- $str = '<br/><pre>' . htmlspecialchars($str) . '</pre>';
- }
- echo "\n" . $str;
- }
- }
-
- /**
- * Adds a message to the log array
- * @param string $msg The message to add
- */
- private function log($msg) {
- $this->log[] = $msg;
- }
-
- /**
- * Prepends the given $msg with the current date and time inside square brackets.
- *
- * @param string $msg
- *
- * @return string
- */
- private function stamp($msg) {
- $date = \DateTime::createFromFormat('U.u', sprintf('%.f', microtime(true)))->format('Y-m-d\TH:i:s.uO');
- $line = '[' . $date . '] ' . $msg;
-
- return $line;
- }
-
- /**
- * Returns the log array
- */
- public function get_log() {
- return $this->log;
- }
-
- /**
- * Truncates the log array
- */
- public function clear_log() {
- $this->log = array();
- }
-}
diff --git a/src/Exceptions/Exception.php b/src/Exceptions/Exception.php
new file mode 100644
index 0000000..353c76e
--- /dev/null
+++ b/src/Exceptions/Exception.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace SMTPValidateEmail\Exceptions;
+
+class Exception extends \Exception
+{
+
+}
diff --git a/src/Exceptions/NoConnection.php b/src/Exceptions/NoConnection.php
new file mode 100644
index 0000000..48c4dee
--- /dev/null
+++ b/src/Exceptions/NoConnection.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace SMTPValidateEmail\Exceptions;
+
+class NoConnection extends Exception
+{
+
+}
diff --git a/src/Exceptions/NoHelo.php b/src/Exceptions/NoHelo.php
new file mode 100644
index 0000000..0c1c0b0
--- /dev/null
+++ b/src/Exceptions/NoHelo.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace SMTPValidateEmail\Exceptions;
+
+class NoHelo extends Exception
+{
+
+}
diff --git a/src/Exceptions/NoMailFrom.php b/src/Exceptions/NoMailFrom.php
new file mode 100644
index 0000000..a3c7b9c
--- /dev/null
+++ b/src/Exceptions/NoMailFrom.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace SMTPValidateEmail\Exceptions;
+
+class NoMailFrom extends Exception
+{
+
+}
diff --git a/src/Exceptions/NoResponse.php b/src/Exceptions/NoResponse.php
new file mode 100644
index 0000000..efa2359
--- /dev/null
+++ b/src/Exceptions/NoResponse.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace SMTPValidateEmail\Exceptions;
+
+class NoResponse extends Exception
+{
+
+}
diff --git a/src/Exceptions/NoTLS.php b/src/Exceptions/NoTLS.php
new file mode 100644
index 0000000..67d9610
--- /dev/null
+++ b/src/Exceptions/NoTLS.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace SMTPValidateEmail\Exceptions;
+
+class NoTLS extends Exception
+{
+
+}
diff --git a/src/Exceptions/NoTimeout.php b/src/Exceptions/NoTimeout.php
new file mode 100644
index 0000000..5abf828
--- /dev/null
+++ b/src/Exceptions/NoTimeout.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace SMTPValidateEmail\Exceptions;
+
+class NoTimeout extends Exception
+{
+
+}
diff --git a/src/Exceptions/SendFailed.php b/src/Exceptions/SendFailed.php
new file mode 100644
index 0000000..2ca9ccd
--- /dev/null
+++ b/src/Exceptions/SendFailed.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace SMTPValidateEmail\Exceptions;
+
+class SendFailed extends Exception
+{
+
+}
diff --git a/src/Exceptions/Timeout.php b/src/Exceptions/Timeout.php
new file mode 100644
index 0000000..12f56ec
--- /dev/null
+++ b/src/Exceptions/Timeout.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace SMTPValidateEmail\Exceptions;
+
+class Timeout extends Exception
+{
+
+}
diff --git a/src/Exceptions/UnexpectedResponse.php b/src/Exceptions/UnexpectedResponse.php
new file mode 100644
index 0000000..83b8f4a
--- /dev/null
+++ b/src/Exceptions/UnexpectedResponse.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace SMTPValidateEmail\Exceptions;
+
+class UnexpectedResponse extends Exception
+{
+
+}
diff --git a/src/Validator.php b/src/Validator.php
new file mode 100644
index 0000000..14bbd7f
--- /dev/null
+++ b/src/Validator.php
@@ -0,0 +1,1101 @@
+<?php
+
+namespace SMTPValidateEmail;
+
+use \SMTPValidateEmail\Exceptions\Exception as Exception;
+use \SMTPValidateEmail\Exceptions\Timeout as TimeoutException;
+use \SMTPValidateEmail\Exceptions\NoTimeout as NoTimeoutException;
+use \SMTPValidateEmail\Exceptions\NoConnection as NoConnectionException;
+use \SMTPValidateEmail\Exceptions\UnexpectedResponse as UnexpectedResponseException;
+use \SMTPValidateEmail\Exceptions\NoHelo as NoHeloException;
+use \SMTPValidateEmail\Exceptions\NoMailFrom as NoMailFromException;
+use \SMTPValidateEmail\Exceptions\NoResponse as NoResponseException;
+use \SMTPValidateEmail\Exceptions\SendFailed as SendFailedException;
+
+class Validator
+{
+
+ public $log = [];
+
+ /**
+ * Print stuff as it happens or not
+ *
+ * @var bool
+ */
+ public $debug = false;
+
+ /**
+ * Default smtp port to connect to
+ *
+ * @var int
+ */
+ public $connect_port = 25;
+
+ /**
+ * Are "catch-all" accounts considered valid or not?
+ * If not, the class checks for a "catch-all" and if it determines the box
+ * has a "catch-all", sets all the emails on that domain as invalid.
+ *
+ * @var bool
+ */
+ public $catchall_is_valid = true;
+
+ /**
+ * Whether to perform the "catch-all" test or not
+ *
+ * @var bool
+ */
+ public $catchall_test = false; // Set to true to perform a catchall test
+
+ /**
+ * Being unable to communicate with the remote MTA could mean an address
+ * is invalid, but it might not, depending on your use case, set the
+ * value appropriately.
+ *
+ * @var bool
+ */
+ public $no_comm_is_valid = false;
+
+ /**
+ * Being unable to connect with the remote host could mean a server
+ * configuration issue, but it might not, depending on your use case,
+ * set the value appropriately.
+ */
+ public $no_conn_is_valid = false;
+
+ /**
+ * Whether "greylisted" responses are considered as valid or invalid addresses
+ *
+ * @var bool
+ */
+ public $greylisted_considered_valid = true;
+
+ /**
+ * Timeout values for various commands (in seconds) per RFC 2821
+ *
+ * @var array
+ */
+ protected $command_timeouts = [
+ 'ehlo' => 120,
+ 'helo' => 120,
+ 'tls' => 180, // start tls
+ 'mail' => 300, // mail from
+ 'rcpt' => 300, // rcpt to,
+ 'rset' => 30,
+ 'quit' => 60,
+ 'noop' => 60
+ ];
+
+ const CRLF = "\r\n";
+
+ // Some smtp response codes
+ const SMTP_CONNECT_SUCCESS = 220;
+ const SMTP_QUIT_SUCCESS = 221;
+ const SMTP_GENERIC_SUCCESS = 250;
+ const SMTP_USER_NOT_LOCAL = 251;
+ const SMTP_CANNOT_VRFY = 252;
+
+ const SMTP_SERVICE_UNAVAILABLE = 421;
+
+ // 450 Requested mail action not taken: mailbox unavailable (e.g.,
+ // mailbox busy or temporarily blocked for policy reasons)
+ const SMTP_MAIL_ACTION_NOT_TAKEN = 450;
+ // 451 Requested action aborted: local error in processing
+ const SMTP_MAIL_ACTION_ABORTED = 451;
+ // 452 Requested action not taken: insufficient system storage
+ const SMTP_REQUESTED_ACTION_NOT_TAKEN = 452;
+
+ // 500 Syntax error (may be due to a denied command)
+ const SMTP_SYNTAX_ERROR = 500;
+ // 502 Comment not implemented
+ const SMTP_NOT_IMPLEMENTED = 502;
+ // 503 Bad sequence of commands (may be due to a denied command)
+ const SMTP_BAD_SEQUENCE = 503;
+
+ // 550 Requested action not taken: mailbox unavailable (e.g., mailbox
+ // not found, no access, or command rejected for policy reasons)
+ const SMTP_MBOX_UNAVAILABLE = 550;
+
+ // 554 Seen this from hotmail MTAs, in response to RSET :(
+ const SMTP_TRANSACTION_FAILED = 554;
+
+ /**
+ * List of response codes considered as "greylisted"
+ *
+ * @var array
+ */
+ private $greylisted = [
+ self::SMTP_MAIL_ACTION_NOT_TAKEN,
+ self::SMTP_MAIL_ACTION_ABORTED,
+ self::SMTP_REQUESTED_ACTION_NOT_TAKEN
+ ];
+
+ /**
+ * Internal states we can be in
+ *
+ * @var array
+ */
+ private $state = [
+ 'helo' => false,
+ 'mail' => false,
+ 'rcpt' => false
+ ];
+
+ /**
+ * Holds the socket connection resource
+ *
+ * @var resource
+ */
+ private $socket;
+
+ /**
+ * Holds all the domains we'll validate accounts on
+ *
+ * @var array
+ */
+ private $domains = [];
+
+ /**
+ * @var array
+ */
+ private $domains_info = [];
+
+ /**
+ * Default connect timeout for each MTA attempted (seconds)
+ *
+ * @var int
+ */
+ private $connect_timeout = 10;
+
+ /**
+ * Default sender username
+ *
+ * @var string
+ */
+ private $from_user = 'user';
+
+ /**
+ * Default sender host
+ *
+ * @var string
+ */
+ private $from_domain = 'localhost';
+
+ /**
+ * The host we're currently connected to
+ *
+ * @var string|null
+ */
+ private $host = null;
+
+ /**
+ * List of validation results
+ *
+ * @var array
+ */
+ private $results = [];
+
+ /**
+ * @param array|string $emails Email(s) to validate
+ * @param string|null $sender Sender's email address
+ */
+ public function __construct($emails = [], $sender = null)
+ {
+ if (!empty($emails)) {
+ $this->setEmails($emails);
+ }
+ if (null !== $sender) {
+ $this->setSender($sender);
+ }
+ }
+
+ /**
+ * Disconnects from the SMTP server if needed to release resources
+ */
+ public function __destruct()
+ {
+ $this->disconnect(false);
+ }
+
+ public function acceptsAnyRecipient($domain)
+ {
+ if (!$this->catchall_test) {
+ return false;
+ }
+
+ $test = 'catch-all-test-' . time();
+ $accepted = $this->rcpt($test . '@' . $domain);
+ if ($accepted) {
+ // Success on a non-existing address is a "catch-all"
+ $this->domains_info[$domain]['catchall'] = true;
+ return true;
+ }
+
+ // Log when we get disconnected while trying catchall detection
+ $this->noop();
+ if (!$this->connected()) {
+ $this->debug('Disconnected after trying a non-existing recipient on ' . $domain);
+ }
+
+ /**
+ * N.B.:
+ * Disconnects are considered as a non-catch-all case this way, but
+ * that might not always be the case.
+ */
+ return false;
+ }
+
+ /**
+ * Performs validation of specified email addresses.
+ *
+ * @param array|string $emails Emails to validate (or a single one as a string)
+ * @param string|null $sender Sender email address
+ * @return array List of emails and their results
+ */
+ public function validate($emails = [], $sender = null)
+ {
+ $this->results = [];
+
+ if (!empty($emails)) {
+ $this->setEmails($emails);
+ }
+ if (null !== $sender) {
+ $this->setSender($sender);
+ }
+
+ if (!is_array($this->domains) || empty($this->domains)) {
+ return $this->results;
+ }
+
+ // Query the MTAs on each domain if we have them
+ foreach ($this->domains as $domain => $users) {
+ $mxs = [];
+
+ // Query the MX records for the current domain
+ list($hosts, $weights) = $this->mxQuery($domain);
+
+ // Sort out the MX priorities
+ foreach ($hosts as $k => $host) {
+ $mxs[$host] = $weights[$k];
+ }
+ asort($mxs);
+
+ // Add the hostname itself with 0 weight (RFC 2821)
+ $mxs[$domain] = 0;
+
+ $this->debug('MX records (' . $domain . '): ' . print_r($mxs, true));
+ $this->domains_info[$domain] = [];
+ $this->domains_info[$domain]['users'] = $users;
+ $this->domains_info[$domain]['mxs'] = $mxs;
+
+ // Try each host, $_weight unused in the foreach body, but array_keys() doesn't guarantee the order
+ foreach ($mxs as $host => $_weight) {
+ // try connecting to the remote host
+ try {
+ $this->connect($host);
+ if ($this->connected()) {
+ break;
+ }
+ } catch (NoConnectionException $e) {
+ // Unable to connect to host, so these addresses are invalid?
+ $this->debug('Unable to connect. Exception caught: ' . $e->getMessage());
+ $this->setDomainResults($users, $domain, $this->no_conn_is_valid);
+ }
+ }
+
+ // Are we connected?
+ if ($this->connected()) {
+ try {
+ // Say helo, and continue if we can talk
+ if ($this->helo()) {
+ // try issuing MAIL FROM
+ if (!$this->mail($this->from_user . '@' . $this->from_domain)) {
+ // MAIL FROM not accepted, we can't talk
+ $this->setDomainResults($users, $domain, $this->no_comm_is_valid);
+ }
+
+ /**
+ * If we're still connected, proceed (cause we might get
+ * disconnected, or banned, or greylisted temporarily etc.)
+ * see mail() for more
+ */
+ if ($this->connected()) {
+ $this->noop();
+
+ // Attempt a catch-all test for the domain (if configured to do so)
+ $is_catchall_domain = $this->acceptsAnyRecipient($domain);
+
+ // If a catchall domain is detected, and we consider
+ // accounts on such domains as invalid, mark all the
+ // users as invalid and move on
+ if ($is_catchall_domain) {
+ if (!$this->catchall_is_valid) {
+ $this->setDomainResults($users, $domain, $this->catchall_is_valid);
+ continue;
+ }
+ }
+
+ // If we're still connected, try issuing rcpts
+ if ($this->connected()) {
+ $this->noop();
+ // RCPT for each user
+ foreach ($users as $user) {
+ $address = $user . '@' . $domain;
+ $this->results[$address] = $this->rcpt($address);
+ $this->noop();
+ }
+ }
+
+ // Saying bye-bye if we're still connected, cause we're done here
+ if ($this->connected()) {
+ // Issue a RSET for all the things we just made the MTA do
+ $this->rset();
+ $this->disconnect();
+ }
+ }
+ } else {
+ // We didn't get a good response to helo and should be disconnected already
+ $this->setDomainResults($users, $domain, $this->no_comm_is_valid);
+ }
+ } catch (UnexpectedResponseException $e) {
+ // Unexpected responses handled as $this->no_comm_is_valid, that way anyone can
+ // decide for themselves if such results are considered valid or not
+ $this->setDomainResults($users, $domain, $this->no_comm_is_valid);
+ } catch (TimeoutException $e) {
+ // A timeout is a comm failure, so treat the results on that domain
+ // according to $this->no_comm_is_valid as well
+ $this->setDomainResults($users, $domain, $this->no_comm_is_valid);
+ }
+ }
+ } // outermost foreach
+
+ return $this->getResults();
+ }
+
+ /**
+ * Get validation results
+ *
+ * @param bool $include_domains_info Whether to include extra info in the results
+ *
+ * @return array
+ */
+ public function getResults($include_domains_info = true)
+ {
+ if ($include_domains_info) {
+ $this->results['domains'] = $this->domains_info;
+ } else {
+ unset($this->results['domains']);
+ }
+
+ return $this->results;
+ }
+
+ /**
+ * Helper to set results for all the users on a domain to a specific value
+ *
+ * @param array $users Users (usernames)
+ * @param string $domain The domain for the users/usernames
+ * @param bool $val Value to set
+ *
+ * @return void
+ */
+ private function setDomainResults(array $users, $domain, $val)
+ {
+ foreach ($users as $user) {
+ $this->results[$user . '@' . $domain] = $val;
+ }
+ }
+
+ /**
+ * Returns true if we're connected to an MTA
+ *
+ * @return bool
+ */
+ protected function connected()
+ {
+ return is_resource($this->socket);
+ }
+
+ /**
+ * Tries to connect to the specified host on the pre-configured port.
+ *
+ * @param string $host Host to connect to
+ *
+ * @throws NoConnectionException
+ * @throws NoTimeoutException
+ *
+ * @return void
+ */
+ protected function connect($host)
+ {
+ $remote_socket = $host . ':' . $this->connect_port;
+ $errnum = 0;
+ $errstr = '';
+ $this->host = $remote_socket;
+
+ // Open connection
+ $this->debug('Connecting to ' . $this->host);
+ // @codingStandardsIgnoreLine
+ $this->socket = /** @scrutinizer ignore-unhandled */ @stream_socket_client(
+ $this->host,
+ $errnum,
+ $errstr,
+ $this->connect_timeout,
+ STREAM_CLIENT_CONNECT,
+ stream_context_create([])
+ );
+
+ // Check and throw if not connected
+ if (!$this->connected()) {
+ $this->debug('Connect failed: ' . $errstr . ', error number: ' . $errnum . ', host: ' . $this->host);
+ throw new NoConnectionException('Cannot open a connection to remote host (' . $this->host . ')');
+ }
+
+ $result = stream_set_timeout($this->socket, $this->connect_timeout);
+ if (!$result) {
+ throw new NoTimeoutException('Cannot set timeout');
+ }
+
+ $this->debug('Connected to ' . $this->host . ' successfully');
+ }
+
+ /**
+ * Disconnects the currently connected MTA.
+ *
+ * @param bool $quit Whether to send QUIT command before closing the socket on our end
+ *
+ * @return void
+ */
+ protected function disconnect($quit = true)
+ {
+ if ($quit) {
+ $this->quit();
+ }
+
+ if ($this->connected()) {
+ $this->debug('Closing socket to ' . $this->host);
+ fclose($this->socket);
+ }
+
+ $this->host = null;
+ $this->resetState();
+ }
+
+ /**
+ * Resets internal state flags to defaults
+ *
+ * @return void
+ */
+ private function resetState()
+ {
+ $this->state['helo'] = false;
+ $this->state['mail'] = false;
+ $this->state['rcpt'] = false;
+ }
+
+ /**
+ * Sends a HELO/EHLO sequence.
+ *
+ * @todo Implement TLS
+ *
+ * @return bool|null True if successful, false otherwise. Null if already done.
+ */
+ protected function helo()
+ {
+ // Don't do it if already done
+ if ($this->state['helo']) {
+ return null;
+ }
+
+ $result = false;
+ try {
+ $this->expect(self::SMTP_CONNECT_SUCCESS, $this->command_timeouts['helo']);
+ $this->ehlo();
+
+ // Session started
+ $this->state['helo'] = true;
+
+ // Are we going for a TLS connection?
+ /*
+ if ($this->tls) {
+ // send STARTTLS, wait 3 minutes
+ $this->send('STARTTLS');
+ $this->expect(self::SMTP_CONNECT_SUCCESS, $this->command_timeouts['tls']);
+ $result = stream_socket_enable_crypto($this->socket, true,
+ STREAM_CRYPTO_METHOD_TLS_CLIENT);
+ if (!$result) {
+ throw new SMTP_Validate_Email_Exception_No_TLS('Cannot enable TLS');
+ }
+ }
+ */
+
+ $result = true;
+ } catch (UnexpectedResponseException $e) {
+ // Connected, but got an unexpected response, so disconnect
+ $result = false;
+ $this->debug('Unexpected response after connecting: ' . $e->getMessage());
+ $this->disconnect(false);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Sends `EHLO` or `HELO`, depending on what's supported by the remote host.
+ *
+ * @return void
+ */
+ protected function ehlo()
+ {
+ try {
+ // Modern
+ $this->send('EHLO ' . $this->from_domain);
+ $this->expect(self::SMTP_GENERIC_SUCCESS, $this->command_timeouts['ehlo']);
+ } catch (UnexpectedResponseException $e) {
+ // Legacy
+ $this->send('HELO ' . $this->from_domain);
+ $this->expect(self::SMTP_GENERIC_SUCCESS, $this->command_timeouts['helo']);
+ }
+ }
+
+ /**
+ * Sends a `MAIL FROM` command which indicates the sender.
+ *
+ * @param string $from The "From:" address
+ *
+ * @throws NoHeloException
+ *
+ * @return bool Whether the command was accepted or not
+ */
+ protected function mail($from)
+ {
+ if (!$this->state['helo']) {
+ throw new NoHeloException('Need HELO before MAIL FROM');
+ }
+
+ // Issue MAIL FROM, 5 minute timeout
+ $this->send('MAIL FROM:<' . $from . '>');
+
+ try {
+ $this->expect(self::SMTP_GENERIC_SUCCESS, $this->command_timeouts['mail']);
+
+ // Set state flags
+ $this->state['mail'] = true;
+ $this->state['rcpt'] = false;
+
+ $result = true;
+ } catch (UnexpectedResponseException $e) {
+ $result = false;
+
+ // Got something unexpected in response to MAIL FROM
+ $this->debug("Unexpected response to MAIL FROM\n:" . $e->getMessage());
+
+ // Hotmail has been known to do this + was closing the connection
+ // forcibly on their end, so we're killing the socket here too
+ $this->disconnect(false);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Sends a RCPT TO command to indicate a recipient.
+ *
+ * @param string $to Recipient's email address
+ * @throws NoMailFromException
+ *
+ * @return bool Whether the recipient was accepted or not
+ */
+ protected function rcpt($to)
+ {
+ // Need to have issued MAIL FROM first
+ if (!$this->state['mail']) {
+ throw new NoMailFromException('Need MAIL FROM before RCPT TO');
+ }
+
+ $valid = false;
+ $expected_codes = [
+ self::SMTP_GENERIC_SUCCESS,
+ self::SMTP_USER_NOT_LOCAL
+ ];
+
+ if ($this->greylisted_considered_valid) {
+ $expected_codes = array_merge($expected_codes, $this->greylisted);
+ }
+
+ // Issue RCPT TO, 5 minute timeout
+ try {
+ $this->send('RCPT TO:<' . $to . '>');
+ // Handle response
+ try {
+ $this->expect($expected_codes, $this->command_timeouts['rcpt']);
+ $this->state['rcpt'] = true;
+ $valid = true;
+ } catch (UnexpectedResponseException $e) {
+ $this->debug('Unexpected response to RCPT TO: ' . $e->getMessage());
+ }
+ } catch (Exception $e) {
+ $this->debug('Sending RCPT TO failed: ' . $e->getMessage());
+ }
+
+ return $valid;
+ }
+
+ /**
+ * Sends a RSET command and resets certain parts of internal state.
+ *
+ * @return void
+ */
+ protected function rset()
+ {
+ $this->send('RSET');
+
+ // MS ESMTP doesn't follow RFC according to ZF tracker, see [ZF-1377]
+ $expected = [
+ self::SMTP_GENERIC_SUCCESS,
+ self::SMTP_CONNECT_SUCCESS,
+ self::SMTP_NOT_IMPLEMENTED,
+ // hotmail returns this o_O
+ self::SMTP_TRANSACTION_FAILED
+ ];
+ $this->expect($expected, $this->command_timeouts['rset'], true);
+ $this->state['mail'] = false;
+ $this->state['rcpt'] = false;
+ }
+
+ /**
+ * Sends a QUIT command.
+ *
+ * @return void
+ */
+ protected function quit()
+ {
+ // Although RFC says QUIT can be issued at any time, we won't
+ if ($this->state['helo']) {
+ $this->send('QUIT');
+ $this->expect(
+ [self::SMTP_GENERIC_SUCCESS,self::SMTP_QUIT_SUCCESS],
+ $this->command_timeouts['quit'],
+ true
+ );
+ }
+ }
+
+ /**
+ * Sends a NOOP command.
+ *
+ * @return void
+ */
+ protected function noop()
+ {
+ $this->send('NOOP');
+
+ /**
+ * The `SMTP` string is here to fix issues with some bad RFC implementations.
+ * Found at least 1 server replying to NOOP without any code.
+ */
+ $expected_codes = [
+ 'SMTP',
+ self::SMTP_BAD_SEQUENCE,
+ self::SMTP_NOT_IMPLEMENTED,
+ self::SMTP_GENERIC_SUCCESS,
+ self::SMTP_SYNTAX_ERROR,
+ self::SMTP_CONNECT_SUCCESS
+ ];
+ $this->expect($expected_codes, $this->command_timeouts['noop'], true);
+ }
+
+ /**
+ * Sends a command to the remote host.
+ *
+ * @param string $cmd The command to send
+ *
+ * @return int|bool Number of bytes written to the stream
+ * @throws NoConnectionException
+ * @throws SendFailedException
+ */
+ protected function send($cmd)
+ {
+ // Must be connected
+ $this->throwIfNotConnected();
+
+ $this->debug('send>>>: ' . $cmd);
+ // Write the cmd to the connection stream
+ $result = fwrite($this->socket, $cmd . self::CRLF);
+
+ // Did it work?
+ if (false === $result) {
+ throw new SendFailedException('Send failed on: ' . $this->host);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Receives a response line from the remote host.
+ *
+ * @param int $timeout Timeout in seconds
+ *
+ * @return string
+ *
+ * @throws NoConnectionException
+ * @throws TimeoutException
+ * @throws NoResponseException
+ */
+ protected function recv($timeout = null)
+ {
+ // Must be connected
+ $this->throwIfNotConnected();
+
+ // Has a custom timeout been specified?
+ if (null !== $timeout) {
+ stream_set_timeout($this->socket, $timeout);
+ }
+
+ // Retrieve response
+ $line = fgets($this->socket, 1024);
+ $this->debug('<<<recv: ' . $line);
+
+ // Have we timed out?
+ $info = stream_get_meta_data($this->socket);
+ if (!empty($info['timed_out'])) {
+ throw new TimeoutException('Timed out in recv');
+ }
+
+ // Did we actually receive anything?
+ if (false === $line) {
+ throw new NoResponseException('No response in recv');
+ }
+
+ return $line;
+ }
+
+ /**
+ * Receives lines from the remote host and looks for expected response codes.
+ *
+ * @param int|int[] $codes List of one or more expected response codes
+ * @param int $timeout The timeout for this individual command, if any
+ * @param bool $empty_response_allowed When true, empty responses are allowed
+ *
+ * @return string The last text message received
+ *
+ * @throws UnexpectedResponseException
+ */
+ protected function expect($codes, $timeout = null, $empty_response_allowed = false)
+ {
+ if (!is_array($codes)) {
+ $codes = (array) $codes;
+ }
+
+ $code = null;
+ $text = '';
+
+ try {
+ $line = $this->recv($timeout);
+ $text = $line;
+ while (preg_match('/^[0-9]+-/', $line)) {
+ $line = $this->recv($timeout);
+ $text .= $line;
+ }
+ sscanf($line, '%d%s', $code, $text);
+ // TODO/FIXME: This is terrible to read/comprehend
+ if ($code == self::SMTP_SERVICE_UNAVAILABLE ||
+ (false === $empty_response_allowed && (null === $code || !in_array($code, $codes)))) {
+ throw new UnexpectedResponseException($line);
+ }
+ } catch (NoResponseException $e) {
+ /**
+ * No response in expect() probably means that the remote server
+ * forcibly closed the connection so lets clean up on our end as well?
+ */
+ $this->debug('No response in expect(): ' . $e->getMessage());
+ $this->disconnect(false);
+ }
+
+ return $text;
+ }
+
+ /**
+ * Splits the email address string into its respective user and domain parts
+ * and returns those as an array.
+ *
+ * @param string $email Email address
+ *
+ * @return array ['user', 'domain']
+ */
+ protected function splitEmail($email)
+ {
+ $parts = explode('@', $email);
+ $domain = array_pop($parts);
+ $user = implode('@', $parts);
+
+ return [$user, $domain];
+ }
+
+ /**
+ * Sets the email addresses that should be validated.
+ *
+ * @param array|string $emails List of email addresses (or a single one a string).
+ *
+ * @return void
+ */
+ public function setEmails($emails)
+ {
+ if (!is_array($emails)) {
+ $emails = (array) $emails;
+ }
+
+ $this->domains = [];
+
+ foreach ($emails as $email) {
+ list($user, $domain) = $this->splitEmail($email);
+ if (!isset($this->domains[$domain])) {
+ $this->domains[$domain] = [];
+ }
+ $this->domains[$domain][] = $user;
+ }
+ }
+
+ /**
+ * Sets the email address to use as the sender/validator.
+ *
+ * @param string $email
+ *
+ * @return void
+ */
+ public function setSender($email)
+ {
+ $parts = $this->splitEmail($email);
+ $this->from_user = $parts[0];
+ $this->from_domain = $parts[1];
+ }
+
+ /**
+ * Queries the DNS server for MX entries of a certain domain.
+ *
+ * @param string $domain The domain for which to retrieve MX records
+ * @return array MX hosts and their weights
+ */
+ protected function mxQuery($domain)
+ {
+ $hosts = [];
+ $weight = [];
+ getmxrr($domain, $hosts, $weight);
+
+ return [$hosts, $weight];
+ }
+
+ /**
+ * Throws if not currently connected.
+ *
+ * @return void
+ * @throws NoConnectionException
+ */
+ private function throwIfNotConnected()
+ {
+ if (!$this->connected()) {
+ throw new NoConnectionException('No connection');
+ }
+ }
+
+ /**
+ * Debug helper. If it detects a CLI env, it just dumps given `$str` on a
+ * new line, otherwise it prints stuff <pre>.
+ *
+ * @param string $str
+ *
+ * @return void
+ */
+ private function debug($str)
+ {
+ $str = $this->stamp($str);
+ $this->log($str);
+ if ($this->debug) {
+ if ('cli' !== PHP_SAPI) {
+ $str = '<br/><pre>' . htmlspecialchars($str) . '</pre>';
+ }
+ echo "\n" . $str;
+ }
+ }
+
+ /**
+ * Adds a message to the log array
+ *
+ * @param string $msg
+ *
+ * @return void
+ */
+ private function log($msg)
+ {
+ $this->log[] = $msg;
+ }
+
+ /**
+ * Prepends the given $msg with the current date and time inside square brackets.
+ *
+ * @param string $msg
+ *
+ * @return string
+ */
+ private function stamp($msg)
+ {
+ $date = \DateTime::createFromFormat('U.u', sprintf('%.f', microtime(true)))->format('Y-m-d\TH:i:s.uO');
+ $line = '[' . $date . '] ' . $msg;
+
+ return $line;
+ }
+
+ /**
+ * Returns the log array
+ *
+ * @return array
+ */
+ public function getLog()
+ {
+ return $this->log;
+ }
+
+ /**
+ * Truncates the log array
+ *
+ * @return void
+ */
+ public function clearLog()
+ {
+ $this->log = [];
+ }
+
+ /**
+ * Compat for old lower_cased method calls.
+ *
+ * @param string $name
+ * @param array $args
+ *
+ * @return void
+ */
+ public function __call($name, $args)
+ {
+ $camelized = self::camelize($name);
+ if (\method_exists($this, $camelized)) {
+ return \call_user_func_array([$this, $camelized], $args);
+ } else {
+ trigger_error('Fatal error: Call to undefined method ' . self::class . '::' . $name . '()', E_USER_ERROR);
+ }
+ }
+
+ /**
+ * Set the desired connect timeout.
+ *
+ * @param int $timeout Connect timeout in seconds
+ *
+ * @return void
+ */
+ public function setConnectTimeout($timeout)
+ {
+ $this->connect_timeout = (int) $timeout;
+ }
+
+ /**
+ * Get the current connect timeout.
+ *
+ * @return int
+ */
+ public function getConnectTimeout()
+ {
+ return $this->connect_timeout;
+ }
+
+ /**
+ * Set connect port.
+ *
+ * @param int $port
+ *
+ * @return void
+ */
+ public function setConnectPort($port)
+ {
+ $this->connect_port = (int) $port;
+ }
+
+ /**
+ * Get current connect port.
+ *
+ * @return int
+ */
+ public function getConnectPort()
+ {
+ return $this->connect_port;
+ }
+
+ /**
+ * Turn on "catch-all" detection.
+ *
+ * @return void
+ */
+ public function enableCatchAllTest()
+ {
+ $this->catchall_test = true;
+ }
+
+ /**
+ * Turn off "catch-all" detection.
+ *
+ * @return void
+ */
+ public function disableCatchAllTest()
+ {
+ $this->catchall_test = false;
+ }
+
+ /**
+ * Returns whether "catch-all" test is to be performed or not.
+ *
+ * @return bool
+ */
+ public function isCatchAllEnabled()
+ {
+ return $this->catchall_test;
+ }
+
+ /**
+ * Set whether "catch-all" results are considered valid or not.
+ *
+ * @param bool $flag When true, "catch-all" accounts are considered valid
+ *
+ * @return void
+ */
+ public function setCatchAllValidity($flag)
+ {
+ $this->catchall_is_valid = (bool) $flag;
+ }
+
+ /**
+ * Get current state of "catch-all" validity flag.
+ *
+ * @return void
+ */
+ public function getCatchAllValidity()
+ {
+ return $this->catchall_is_valid;
+ }
+
+ /**
+ * Camelizes a string.
+ *
+ * @param string $id A string to camelize
+ *
+ * @return string The camelized string
+ */
+ private static function camelize($id)
+ {
+ return strtr(
+ ucwords(
+ strtr(
+ $id,
+ ['_' => ' ', '.' => '_ ', '\\' => '_ ']
+ )
+ ),
+ [' ' => '']
+ );
+ }
+}
diff --git a/tests/Functional/ValidatorTest.php b/tests/Functional/ValidatorTest.php
new file mode 100644
index 0000000..35e2aa2
--- /dev/null
+++ b/tests/Functional/ValidatorTest.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace SMTPValidateEmail\Tests\Functional;
+
+use SMTPValidateEmail\Validator;
+use SMTPValidateEmail\Tests\TestCase;
+use SMTPValidateEmail\Tests\SmtpSinkServerProcess;
+
+/**
+ * Functional tests for Validator class.
+ */
+class ValidatorTest extends TestCase
+{
+ public function testNoConnIsValid()
+ {
+ // Testing no connection, so using a non-existent host on .localhost
+ $uniq = uniqid();
+ $host = 'localhost';
+ $email = 'test@' . $uniq . '.' . $host;
+ $timeout = 1;
+
+ // Default should be false
+ $inst = new Validator($email, 'sender@localhost');
+ $inst->setConnectTimeout($timeout);
+ $this->assertFalse($inst->no_conn_is_valid);
+ $results = $inst->validate();
+ $this->assertFalse($results[$email]);
+
+ $log = $inst->getLog();
+ $last_line = \array_pop($log);
+ $needle = 'Unable to connect. Exception caught: Cannot open a connection to remote host';
+ $this->assertContains($needle, $last_line);
+
+ // When changed, it should change the returned result
+ $inst->no_conn_is_valid = true;
+
+ $results = $inst->validate();
+ $this->assertTrue($results[$email]);
+ }
+
+ /**
+ * Requires having node-smtp-sink (`npm install smtp-sink`) locally.
+ * It needs to be ran with -w switch: `smtp-sink -w allowed-sender@example.org`
+ */
+ public function testNoCommIsValidWithLocalSmtpSinkWhitelisted()
+ {
+ // Mark skipped if smtp-sink is not running
+ if (!$this->isSmtpSinkRunning()) {
+ $this->markTestSkipped('smtp-sink is not running.');
+ }
+
+ $email = 'test@localhost';
+
+ $inst = new Validator($email, 'not-allowed@example.org');
+ $inst->setConnectPort(1025);
+ $inst->setConnectTimeout(1);
+ $this->assertFalse($inst->no_comm_is_valid);
+ $results = $inst->validate();
+ $this->assertFalse($results[$email]);
+
+ $inst->no_comm_is_valid = true;
+ $results = $inst->validate($email, 'not-allowed@example.org');
+ $this->assertTrue($results[$email]);
+ }
+
+ public function testValidSenderWithLocalSmtpSinkWhitelisted()
+ {
+ // Mark skipped if smtp-sink is not running
+ if (!$this->isSmtpSinkRunning()) {
+ $this->markTestSkipped('smtp-sink is not running.');
+ }
+
+ $email = 'test@localhost';
+
+ $inst = new Validator($email, 'allowed-sender@example.org');
+ $inst->setConnectTimeout(1);
+ $inst->setConnectPort(1025);
+ $results = $inst->validate();
+
+ $this->assertTrue($results[$email]);
+ }
+
+ public function testCatchAllConsideredValid()
+ {
+ // Mark skipped if smtp-sink is not running
+ if (!$this->isSmtpSinkRunning()) {
+ $this->markTestSkipped('smtp-sink is not running.');
+ }
+
+ $emails = [
+ 'user@localhost',
+ 'tester@localhost'
+ ];
+
+ $inst = new Validator($emails, 'allowed-sender@example.org');
+ $inst->setConnectTimeout(1);
+ $inst->setConnectPort(1025);
+ $inst->enableCatchAllTest();
+
+ $results = $inst->validate();
+ foreach ($emails as $email) {
+ $this->assertTrue($results[$email]);
+ }
+ }
+
+ public function testCatchAllConsideredInvalid()
+ {
+ // Mark skipped if smtp-sink is not running
+ if (!$this->isSmtpSinkRunning()) {
+ $this->markTestSkipped('smtp-sink is not running.');
+ }
+
+ $email = 'test@localhost';
+
+ $inst = new Validator($email, 'allowed-sender@example.org');
+ $inst->setConnectTimeout(1);
+ $inst->setConnectPort(1025);
+ $inst->enableCatchAllTest();
+
+ // If a catch-all is detected, the results are not considered valid
+ $inst->setCatchAllValidity(false);
+
+ $results = $inst->validate();
+ $this->assertFalse($results[$email]);
+ }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..a83ee95
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace SMTPValidateEmail\Tests;
+
+/**
+ * Abstract base class for all test case implementations.
+ *
+ * @package ZWF\GoogleFontsOptimizer\Tests
+ */
+abstract class TestCase extends \PHPUnit_Framework_TestCase
+{
+ protected function isSmtpSinkRunning()
+ {
+ $running = false;
+
+ // @codingStandardsIgnoreLine
+ $fp = /** @scrutinizer ignore-unhandled */ @fsockopen('localhost', 1025);
+ if (false !== $fp) {
+ $running = true;
+ fclose($fp);
+ }
+
+ return $running;
+ }
+}
diff --git a/tests/Unit/ValidatorTest.php b/tests/Unit/ValidatorTest.php
new file mode 100644
index 0000000..a0a4de4
--- /dev/null
+++ b/tests/Unit/ValidatorTest.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace SMTPValidateEmail\Tests\Unit;
+
+use \SMTPValidateEmail\Tests\TestCase;
+use \SMTPValidateEmail\Validator;
+
+/**
+ * Test cases for the Validator class.
+ */
+class ValidatorTest extends TestCase
+{
+ /**
+ * Test exceptions exist and are throwable
+ */
+ public function testExceptions()
+ {
+ $ns = '\\SMTPValidateEmail\\Exceptions';
+ $exceptions = [
+ 'Exception',
+ 'NoConnection',
+ 'NoHelo',
+ 'NoMailFrom',
+ 'NoResponse',
+ 'NoTimeout',
+ 'NoTLS',
+ 'SendFailed',
+ 'Timeout',
+ 'UnexpectedResponse'
+ ];
+ foreach ($exceptions as $name) {
+ $fqcn = $ns . '\\' . $name;
+ $this->expectException($fqcn);
+ $exc = new $fqcn();
+ throw $exc;
+ }
+ }
+
+ public function testOldStyleMethods()
+ {
+ $instance = new Validator();
+ $this->assertSame([], $instance->get_log(), 'old get_log() method still works');
+
+ $instance->log[] = 'test';
+ $this->assertContains('test', $instance->log, 'log contains `test`');
+
+ $instance->clear_log();
+ $this->assertSame([], $instance->log, 'log is cleared via old clear_log() method call');
+ $this->assertNotContains('test', $instance->log, '`test` does not exist in log any more');
+
+ // Unknown methods trigger E_USER_ERROR
+ $this->expectException(\PHPUnit_Framework_Error::class);
+ $instance->undefined_method();
+ }
+
+ public function testGetResults()
+ {
+ $inst = new Validator();
+ $results1 = $inst->get_results(); // test old style method call while at it
+ $results2 = $inst->getResults(false);
+
+ $this->assertArrayHasKey('domains', $results1, '`domains` key is missing.');
+ $this->assertArrayNotHasKey('domains', $results2, '`domains` key present, but it shouldnt be.');
+ $this->assertSame([], $results2, 'getResults() is not empty');
+ }
+
+ public function testSetEmails()
+ {
+ $emails = [
+ 'email@example.org',
+ 'some@some-other-example.org'
+ ];
+ $expected = [
+ 'example.org' => ['email'],
+ 'some-other-example.org' => ['some']
+ ];
+
+ $inst = new Validator($emails);
+ $this->assertAttributeEquals($expected, 'domains', $inst);
+ $inst->setEmails([]);
+ $this->assertAttributeEquals([], 'domains', $inst);
+ $inst->setEmails($emails);
+ $this->assertAttributeEquals($expected, 'domains', $inst);
+
+ // Test setEmails with a single string
+ $inst->setEmails('test@example.org');
+ $this->assertAttributeEquals(['example.org' => ['test']], 'domains', $inst);
+ }
+
+ public function testSetSender()
+ {
+ $inst = new Validator();
+ $this->assertAttributeEquals('user', 'from_user', $inst);
+ $this->assertAttributeEquals('localhost', 'from_domain', $inst);
+
+ $inst = new Validator([], 'email@example.org');
+ $this->assertAttributeEquals('email', 'from_user', $inst);
+ $this->assertAttributeEquals('example.org', 'from_domain', $inst);
+ }
+
+ public function testValidateMethodWithEmptyParams()
+ {
+ $inst = new Validator();
+ $inst->validate();
+ $this->assertAttributeEquals('user', 'from_user', $inst);
+ $this->assertAttributeEquals('localhost', 'from_domain', $inst);
+ }
+
+ public function testSomeSettersGetters()
+ {
+ $inst = new Validator();
+
+ // Defaults
+ $this->assertSame(25, $inst->getConnectPort());
+ $this->assertSame(10, $inst->getConnectTimeout());
+ $this->assertFalse($inst->isCatchAllEnabled());
+ $this->assertTrue($inst->getCatchAllValidity());
+
+ $inst->setConnectPort(1025);
+ $this->assertSame(1025, $inst->getConnectPort());
+
+ $inst->setConnectTimeout(1);
+ $this->assertSame(1, $inst->getConnectTimeout());
+
+ $inst->enableCatchAllTest();
+ $this->assertTrue($inst->isCatchAllEnabled());
+
+ $inst->disableCatchAllTest();
+ $this->assertFalse($inst->isCatchAllEnabled());
+
+ $inst->setCatchAllValidity(false);
+ $this->assertFalse($inst->getCatchAllValidity());
+ }
+
+ public function testDebugPrint()
+ {
+ $inst = new Validator('test@invalid.localhost', 'sender@localhost');
+ $inst->debug = true;
+ $results = $inst->validate();
+ $this->expectOutputRegex('/connecting to invalid.localhost:25/i');
+ $this->expectOutputRegex('/unable to connect\. exception caught:/i');
+ }
+}