From 6a331bc6bff16e3094bba52f900c50345b4d7853 Mon Sep 17 00:00:00 2001 From: tinect Date: Sun, 4 Jan 2026 21:50:58 +0100 Subject: [PATCH 1/5] feat: use and cache the checks dedicated --- .github/workflows/check.yml | 38 ++++++++ README.md | 10 +- composer.json | 20 ++-- src/Constraint/TestEmail.php | 2 +- src/Constraint/TestEmailValidator.php | 10 +- ...idation.php => FroshMailAddressTester.php} | 4 +- src/Resources/config/services.yaml | 4 +- src/Service/Tester.php | 97 +++++++++++++++++++ src/Service/Validator.php | 70 ------------- .../BuildValidationEventListener.php | 4 +- 10 files changed, 162 insertions(+), 97 deletions(-) create mode 100644 .github/workflows/check.yml rename src/{FroshMailValidation.php => FroshMailAddressTester.php} (66%) create mode 100644 src/Service/Tester.php delete mode 100644 src/Service/Validator.php diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..61365be --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,38 @@ +name: Check +on: + schedule: + - cron: "5 15 * * *" + workflow_dispatch: + pull_request: + push: + branches: + - main + +jobs: + cs: + if: github.event_name != 'schedule' + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Run CS + uses: shopware/github-actions/extension-verifier@main + with: + action: format + + check: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + version-selection: [ 'lowest', 'highest'] + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Run Check + uses: shopware/github-actions/extension-verifier@main + with: + action: check + check-against: ${{ matrix.version-selection }} \ No newline at end of file diff --git a/README.md b/README.md index 41fcfd9..a20fff4 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,24 @@ -# FroshMailValidation +# FroshMailAddressTester -This plugin for Shopware 6 validates email addresses during customer registration and checkout processes to ensure the mailbox is accessible and potentially valid. +This plugin for Shopware 6 tests email address during customer registration and checkout processes to ensure the mailbox is accessible and potentially valid. ## Installation ### Via Composer ```bash -composer require frosh/mail-validation +composer require frosh/mail-address-tester ``` ```bash bin/console plugin:refresh -bin/console plugin:install --activate FroshMailValidation +bin/console plugin:install --activate FroshMailAddressTester bin/console cache:clear ``` ## Support -- **GitHub Issues**: [https://github.com/FriendsOfShopware/FroshMailValidation/issues](https://github.com/FriendsOfShopware/FroshMailValidation/issues) +- **GitHub Issues**: [https://github.com/FriendsOfShopware/FroshMailAddressTester/issues](https://github.com/FriendsOfShopware/FroshMailAddressTester/issues) ## License diff --git a/composer.json b/composer.json index 3041c7a..81bb8ca 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "frosh/mail-validation", + "name": "frosh/mail-address-tester", "description": "Validates email addresses by checking their actual existence on the target mail server. Reduces bounces, fake addresses, and sustainably improves deliverability.", "type": "shopware-platform-plugin", "version": "1.0.0", @@ -10,32 +10,32 @@ ], "require": { "shopware/core": "~6.7", - "shyim/check-if-email-exists": "^1.0" + "shyim/check-if-email-exists": "^1.0.2" }, "license": "MIT", "autoload": { "psr-4": { - "Frosh\\MailValidation\\": "src/" + "Frosh\\MailAddressTester\\": "src/" } }, "extra": { - "shopware-plugin-class": "Frosh\\MailValidation\\FroshMailValidation", + "shopware-plugin-class": "Frosh\\MailAddressTester\\FroshMailAddressTester", "copyright": "FriendsOfShopware", "label": { - "de-DE": "E-Mail-Validierung durch Überprüfung der Existenz", - "en-GB": "MailValidation by checking existence" + "de-DE": "E-Mail-Adressen-Tester", + "en-GB": "Mail address tester" }, "description": { "de-DE": "Validiert E-Mail-Adressen durch Prüfung der tatsächlichen Existenz beim Zielserver. Reduziert Bounces, Fake-Adressen und verbessert nachhaltig die Zustellbarkeit.", "en-GB": "Validates email addresses by checking their actual existence on the target mail server. Reduces bounces, fake addresses, and sustainably improves deliverability." }, "manufacturerLink": { - "de-DE": "https://github.com/FriendsOfShopware/FroshMailValidation/", - "en-GB": "https://github.com/FriendsOfShopware/FroshMailValidation/" + "de-DE": "https://github.com/FriendsOfShopware/FroshMailAddressTester/", + "en-GB": "https://github.com/FriendsOfShopware/FroshMailAddressTester/" }, "supportLink": { - "de-DE": "https://github.com/FriendsOfShopware/FroshMailValidation/", - "en-GB": "https://github.com/FriendsOfShopware/FroshMailValidation/" + "de-DE": "https://github.com/FriendsOfShopware/FroshMailAddressTester/", + "en-GB": "https://github.com/FriendsOfShopware/FroshMailAddressTester/" } }, "scripts": { diff --git a/src/Constraint/TestEmail.php b/src/Constraint/TestEmail.php index 1b0e732..013cb3d 100644 --- a/src/Constraint/TestEmail.php +++ b/src/Constraint/TestEmail.php @@ -1,6 +1,6 @@ 'frosh-mail-validation'])] +#[AutoconfigureTag('monolog.logger', ['channel' => 'frosh-mail-address-tester'])] #[AutoconfigureTag(name: 'validator.constraint_validator')] class TestEmailValidator extends ConstraintValidator { public function __construct( - private readonly Validator $emailValidator, + private readonly Tester $emailAddressTester, ) { } @@ -26,7 +26,7 @@ public function validate(mixed $value, Constraint $constraint): void return; } - if ($this->emailValidator->validateEmail($value)) { + if ($this->emailAddressTester->validateEmail($value)) { return; } diff --git a/src/FroshMailValidation.php b/src/FroshMailAddressTester.php similarity index 66% rename from src/FroshMailValidation.php rename to src/FroshMailAddressTester.php index cd58e19..33db6a2 100644 --- a/src/FroshMailValidation.php +++ b/src/FroshMailAddressTester.php @@ -1,10 +1,10 @@ cache->getItem($emailCacheKey); + + $mailValidCacheResult = $mailValidCache->get(); + if (\is_bool($mailValidCacheResult)) { + return $mailValidCacheResult; + } + + $mailValidCache->expiresAfter(3600); + + $syntaxCheck = new Syntax($email); + if ($syntaxCheck->isValid() === false) { + return false; + } + + $domain = $syntaxCheck->domain; + + $domainCacheKey = 'frosh_mail_validation_domain_' . Hasher::hash($domain); + + $domainValidCache = $this->cache->getItem($domainCacheKey); + + // first check if the domain is already marked as invalid + if ($domainValidCache->get() === false) { + return false; + } + + $domainValidCache->expiresAfter(3600); + + $mxRecords = (new DNS())->getMxRecords($domain); + + if (empty($mxRecords)) { + $domainValidCache->set(false); + $this->cache->save($domainValidCache); + + $this->froshMailAddressTesterLogger->error(\sprintf('Domain %s has no mx records', $domain)); + + return false; + } + + $verifyEmail = $this->systemConfigService->getString('FroshMailAddressTester.config.verifyEmail'); + + $smtpCheck = (new SMTP($verifyEmail))->check($domain, $mxRecords, $email); + if ($smtpCheck->canConnect === false) { + $domainValidCache->set(false); + $this->cache->save($domainValidCache); + + $this->froshMailAddressTesterLogger->error($smtpCheck->error); + + return false; + } + + $domainValidCache->expiresAfter(86400); + $domainValidCache->set(true); + $this->cache->save($domainValidCache); + + $isValid = $smtpCheck->isDeliverable === true && $smtpCheck->isDisabled === false && $smtpCheck->hasFullInbox === false; + + $mailValidCache->set($isValid); + $this->cache->save($mailValidCache); + + if ($isValid === false) { + $this->froshMailAddressTesterLogger->error( + \sprintf('Email address "%s" test failed', $email), + json_decode(json_encode($smtpCheck, \JSON_THROW_ON_ERROR), true, 1, \JSON_THROW_ON_ERROR) + ); + } + + return $isValid; + } +} diff --git a/src/Service/Validator.php b/src/Service/Validator.php deleted file mode 100644 index 7e855d3..0000000 --- a/src/Service/Validator.php +++ /dev/null @@ -1,70 +0,0 @@ -systemConfigService->getString('FroshMailValidation.config.verifyEmail'); - - $syntax = new Syntax($email); - if ($syntax->isValid() === false) { - return false; - } - - $domain = $syntax->domain; - - $domainCacheKey = 'frosh_mail_validation_domain_' . Hasher::hash($domain); - - \assert($this->cache instanceof AdapterInterface); - $domainValidCache = $this->cache->getItem($domainCacheKey); - - // first check if the domain is already marked as invalid - if ($domainValidCache->get() === false) { - return false; - } - - $checker = new EmailChecker(smtp: new SMTP($verifyEmail)); - - $emailCacheKey = 'frosh_mail_validation_email_' . Hasher::hash($email); - - return $this->cache->get($emailCacheKey, function (ItemInterface $cacheItem) use ($email, $checker, $domainValidCache) { - $result = $checker->check($email); - - $domainValid = $result->hasMxRecords && $result->isReachable; - $domainValidCache->set($domainValid); - $domainValidCache->expiresAfter($domainValid ? 86400 : 3600); - $this->cache->save($domainValidCache); - - $cacheItem->expiresAfter(3600); - - if ($domainValid && $result->isDisabled === false && $result->hasFullInbox === false) { - return true; - } - - $this->froshMailValidationLogger->error('Email validation failed', $result->toArray()); - - return false; - }); - } -} diff --git a/src/Subscriber/BuildValidationEventListener.php b/src/Subscriber/BuildValidationEventListener.php index 15c0a5e..989a41a 100644 --- a/src/Subscriber/BuildValidationEventListener.php +++ b/src/Subscriber/BuildValidationEventListener.php @@ -1,8 +1,8 @@ Date: Wed, 4 Feb 2026 13:40:32 +0100 Subject: [PATCH 2/5] feat: add dedicated methods for cache operations --- src/Service/Tester.php | 49 +++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/src/Service/Tester.php b/src/Service/Tester.php index 85b37c4..7249dd8 100644 --- a/src/Service/Tester.php +++ b/src/Service/Tester.php @@ -2,6 +2,7 @@ namespace Frosh\MailAddressTester\Service; +use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; use Psr\Log\LoggerInterface; use Shopware\Core\Framework\Util\Hasher; @@ -25,16 +26,12 @@ public function validateEmail(string $email): bool { $email = \strtolower($email); - $emailCacheKey = 'frosh_mail_validation_email_' . Hasher::hash($email); - $mailValidCache = $this->cache->getItem($emailCacheKey); - + $mailValidCache = $this->getCacheItem($email); $mailValidCacheResult = $mailValidCache->get(); if (\is_bool($mailValidCacheResult)) { return $mailValidCacheResult; } - $mailValidCache->expiresAfter(3600); - $syntaxCheck = new Syntax($email); if ($syntaxCheck->isValid() === false) { return false; @@ -42,22 +39,16 @@ public function validateEmail(string $email): bool $domain = $syntaxCheck->domain; - $domainCacheKey = 'frosh_mail_validation_domain_' . Hasher::hash($domain); - - $domainValidCache = $this->cache->getItem($domainCacheKey); - + $domainValidCache = $this->getCacheItem($domain); // first check if the domain is already marked as invalid if ($domainValidCache->get() === false) { return false; } - $domainValidCache->expiresAfter(3600); - $mxRecords = (new DNS())->getMxRecords($domain); if (empty($mxRecords)) { - $domainValidCache->set(false); - $this->cache->save($domainValidCache); + $this->saveCache($domainValidCache, false); $this->froshMailAddressTesterLogger->error(\sprintf('Domain %s has no mx records', $domain)); @@ -67,23 +58,17 @@ public function validateEmail(string $email): bool $verifyEmail = $this->systemConfigService->getString('FroshMailAddressTester.config.verifyEmail'); $smtpCheck = (new SMTP($verifyEmail))->check($domain, $mxRecords, $email); - if ($smtpCheck->canConnect === false) { - $domainValidCache->set(false); - $this->cache->save($domainValidCache); + $this->saveCache($domainValidCache, $smtpCheck->canConnect); + if ($smtpCheck->canConnect === false) { $this->froshMailAddressTesterLogger->error($smtpCheck->error); return false; } - $domainValidCache->expiresAfter(86400); - $domainValidCache->set(true); - $this->cache->save($domainValidCache); - $isValid = $smtpCheck->isDeliverable === true && $smtpCheck->isDisabled === false && $smtpCheck->hasFullInbox === false; - $mailValidCache->set($isValid); - $this->cache->save($mailValidCache); + $this->saveCache($mailValidCache, $isValid); if ($isValid === false) { $this->froshMailAddressTesterLogger->error( @@ -94,4 +79,24 @@ public function validateEmail(string $email): bool return $isValid; } + + private function getCacheItem(string $value): CacheItemInterface + { + $cacheKey = 'frosh_mail_tester_' . Hasher::hash($value); + + return $this->cache->getItem($cacheKey); + } + + private function saveCache(CacheItemInterface $item, bool $value): void + { + $cacheTime = 3600; + + if ($value === true) { + $cacheTime = 86400; // one day for valid emails + } + + $item->expiresAfter($cacheTime); + $item->set($value); + $this->cache->save($item); + } } From 284ecc3638412812b21c98cf22fa01ae320b48a6 Mon Sep 17 00:00:00 2001 From: tinect Date: Wed, 4 Feb 2026 13:50:16 +0100 Subject: [PATCH 3/5] feat: add monolog config --- src/Constraint/TestEmailValidator.php | 2 +- src/FroshMailAddressTester.php | 26 ++++++++++++++++++++++ src/Resources/config/packages/monolog.yaml | 10 +++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/Resources/config/packages/monolog.yaml diff --git a/src/Constraint/TestEmailValidator.php b/src/Constraint/TestEmailValidator.php index f3d3921..6063c85 100644 --- a/src/Constraint/TestEmailValidator.php +++ b/src/Constraint/TestEmailValidator.php @@ -7,7 +7,7 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; -#[AutoconfigureTag('monolog.logger', ['channel' => 'frosh-mail-address-tester'])] +#[AutoconfigureTag('monolog.logger', ['channel' => 'frosh_mail_tester'])] #[AutoconfigureTag(name: 'validator.constraint_validator')] class TestEmailValidator extends ConstraintValidator { diff --git a/src/FroshMailAddressTester.php b/src/FroshMailAddressTester.php index 33db6a2..afdb87e 100644 --- a/src/FroshMailAddressTester.php +++ b/src/FroshMailAddressTester.php @@ -3,9 +3,35 @@ namespace Frosh\MailAddressTester; use Shopware\Core\Framework\Plugin; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\Config\Loader\DelegatingLoader; +use Symfony\Component\Config\Loader\LoaderResolver; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\DirectoryLoader; +use Symfony\Component\DependencyInjection\Loader\GlobFileLoader; +use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; class FroshMailAddressTester extends Plugin { + public function build(ContainerBuilder $container): void + { + parent::build($container); + + $locator = new FileLocator('Resources/config'); + + $resolver = new LoaderResolver([ + new YamlFileLoader($container, $locator), + new GlobFileLoader($container, $locator), + new DirectoryLoader($container, $locator), + ]); + + $configLoader = new DelegatingLoader($resolver); + + $confDir = \rtrim($this->getPath(), '/') . '/Resources/config'; + + $configLoader->load($confDir . '/{packages}/*.yaml', 'glob'); + } + public function executeComposerCommands(): bool { return true; diff --git a/src/Resources/config/packages/monolog.yaml b/src/Resources/config/packages/monolog.yaml new file mode 100644 index 0000000..71cec37 --- /dev/null +++ b/src/Resources/config/packages/monolog.yaml @@ -0,0 +1,10 @@ +monolog: + channels: ['frosh_mail_tester'] + + handlers: + FroshMailTesterHandler: + type: rotating_file + max_files: 14 + path: '%kernel.logs_dir%/frosh_mail_tester_%kernel.environment%.log' + level: error + channels: ['frosh_mail_tester'] From 4beb4f785e3274c2bff2848ed2f0b38b60937bff Mon Sep 17 00:00:00 2001 From: tinect Date: Wed, 4 Feb 2026 16:00:19 +0100 Subject: [PATCH 4/5] fix: init smtp check just with given verifyEmail --- src/Service/Tester.php | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Service/Tester.php b/src/Service/Tester.php index 7249dd8..539fa84 100644 --- a/src/Service/Tester.php +++ b/src/Service/Tester.php @@ -32,12 +32,12 @@ public function validateEmail(string $email): bool return $mailValidCacheResult; } - $syntaxCheck = new Syntax($email); - if ($syntaxCheck->isValid() === false) { + $syntaxResult = new Syntax($email); + if ($syntaxResult->isValid() === false) { return false; } - $domain = $syntaxCheck->domain; + $domain = $syntaxResult->domain; $domainValidCache = $this->getCacheItem($domain); // first check if the domain is already marked as invalid @@ -56,24 +56,29 @@ public function validateEmail(string $email): bool } $verifyEmail = $this->systemConfigService->getString('FroshMailAddressTester.config.verifyEmail'); + if ($verifyEmail !== '') { + $smtpCheck = new SMTP($verifyEmail); + } else { + $smtpCheck = new SMTP(); + } - $smtpCheck = (new SMTP($verifyEmail))->check($domain, $mxRecords, $email); - $this->saveCache($domainValidCache, $smtpCheck->canConnect); + $smtpResult = $smtpCheck->check($domain, $mxRecords, $email); + $this->saveCache($domainValidCache, $smtpResult->canConnect); - if ($smtpCheck->canConnect === false) { - $this->froshMailAddressTesterLogger->error($smtpCheck->error); + if ($smtpResult->canConnect === false) { + $this->froshMailAddressTesterLogger->error($smtpResult->error); return false; } - $isValid = $smtpCheck->isDeliverable === true && $smtpCheck->isDisabled === false && $smtpCheck->hasFullInbox === false; + $isValid = $smtpResult->isDeliverable === true && $smtpResult->isDisabled === false && $smtpResult->hasFullInbox === false; $this->saveCache($mailValidCache, $isValid); if ($isValid === false) { $this->froshMailAddressTesterLogger->error( \sprintf('Email address "%s" test failed', $email), - json_decode(json_encode($smtpCheck, \JSON_THROW_ON_ERROR), true, 1, \JSON_THROW_ON_ERROR) + json_decode(json_encode($smtpResult, \JSON_THROW_ON_ERROR), true, 1, \JSON_THROW_ON_ERROR) ); } From e54b0d4bfd6ca595751aa14ffa8eeefb01fdc3a8 Mon Sep 17 00:00:00 2001 From: tinect Date: Sat, 21 Feb 2026 14:03:46 +0100 Subject: [PATCH 5/5] feat: add option to specify the option --- src/Resources/config/config.xml | 35 ++++++++++++++++++++++++++++++--- src/Service/Tester.php | 4 ++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/Resources/config/config.xml b/src/Resources/config/config.xml index 79f9c3c..0cb7852 100644 --- a/src/Resources/config/config.xml +++ b/src/Resources/config/config.xml @@ -6,13 +6,42 @@ Basic Configuration Grundeinstellungen + + level + + + simple + simple: just checks if the domain has an announced mail server (MX record) +
+ SMTP: checks if an email would be deliverable by connecting to the mail server and simulating a mail delivery without really sending. Diese Prüfung dauert länger. Make sure the email address provided in the next field is valid, otherwise, some checks will be false-negative. + ]]>
+ Einfach: Es wird lediglich geprüft, ob für die Domain ein bekanntgegebener Mailserver (MX-Eintrag) existiert. +
+ SMTP: Diese Option prüft die Zustellbarkeit einer E-Mail, indem sie eine Verbindung zum Mailserver herstellt und die Zustellung simuliert, ohne die E-Mail tatsächlich zu senden. Diese Prüfung dauert länger. Stellen Sie sicher, dass die im nächsten Feld angegebene E-Mail-Adresse gültig ist, da es sonst zu Fehlalarmen kommen kann. + ]]>
+ + + + +
+ verifyEmail - + verify@example.com - This is important to be a valid mail domain. Otherwise, some checks will be false-positive. - Es ist wichtig, dass es sich um eine gültige E-Mail-Domain handelt. Andernfalls liefern einige Prüfungen falsch-positive Ergebnisse. + This is important to be a valid mail domain. Otherwise, some checks will be false-negative. + Es ist wichtig, dass es sich um eine gültige E-Mail-Domain handelt. Andernfalls liefern einige Prüfungen falsch-negative Ergebnisse. \ No newline at end of file diff --git a/src/Service/Tester.php b/src/Service/Tester.php index 539fa84..80b513e 100644 --- a/src/Service/Tester.php +++ b/src/Service/Tester.php @@ -55,6 +55,10 @@ public function validateEmail(string $email): bool return false; } + if ($this->systemConfigService->getString('FroshMailAddressTester.config.level') !== 'smtp') { + return true; + } + $verifyEmail = $this->systemConfigService->getString('FroshMailAddressTester.config.verifyEmail'); if ($verifyEmail !== '') { $smtpCheck = new SMTP($verifyEmail);