feat(ocp): add email address validator

Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
This commit is contained in:
Daniel Kesselberg 2025-07-06 21:24:45 +02:00
parent e0a21e5927
commit 336c6d2957
No known key found for this signature in database
GPG key ID: 4A81C29F63464E8F
11 changed files with 190 additions and 43 deletions

View file

@ -371,6 +371,12 @@
</UndefinedMethod>
</file>
<file src="apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php">
<DeprecatedMethod>
<code><![CDATA[validateMailAddress]]></code>
<code><![CDATA[validateMailAddress]]></code>
<code><![CDATA[validateMailAddress]]></code>
<code><![CDATA[validateMailAddress]]></code>
</DeprecatedMethod>
<LessSpecificReturnStatement>
<code><![CDATA[$emailAddresses]]></code>
</LessSpecificReturnStatement>
@ -452,6 +458,9 @@
</DeprecatedMethod>
</file>
<file src="apps/dav/lib/CalDAV/Schedule/IMipPlugin.php">
<DeprecatedMethod>
<code><![CDATA[validateMailAddress]]></code>
</DeprecatedMethod>
<RedundantCast>
<code><![CDATA[(string)$iTipMessage->recipientName]]></code>
</RedundantCast>
@ -864,6 +873,11 @@
<code><![CDATA[getUserFolder]]></code>
</DeprecatedMethod>
</file>
<file src="apps/dav/lib/Listener/CalendarContactInteractionListener.php">
<DeprecatedMethod>
<code><![CDATA[validateMailAddress]]></code>
</DeprecatedMethod>
</file>
<file src="apps/dav/lib/Migration/BuildCalendarSearchIndex.php">
<DeprecatedMethod>
<code><![CDATA[getAppValue]]></code>
@ -1572,6 +1586,7 @@
</DeprecatedClass>
<DeprecatedMethod>
<code><![CDATA[getAppValue]]></code>
<code><![CDATA[validateMailAddress]]></code>
</DeprecatedMethod>
<RedundantCast>
<code><![CDATA[(int)$code]]></code>
@ -2079,6 +2094,7 @@
<code><![CDATA[getAppValue]]></code>
<code><![CDATA[getAppValue]]></code>
<code><![CDATA[setAppValue]]></code>
<code><![CDATA[validateMailAddress]]></code>
</DeprecatedMethod>
</file>
<file src="apps/settings/lib/Controller/WebAuthnController.php">
@ -2201,6 +2217,10 @@
</DeprecatedMethod>
</file>
<file src="apps/sharebymail/lib/ShareByMailProvider.php">
<DeprecatedMethod>
<code><![CDATA[validateMailAddress]]></code>
<code><![CDATA[validateMailAddress]]></code>
</DeprecatedMethod>
<InvalidArgument>
<code><![CDATA[$share->getId()]]></code>
<code><![CDATA[(int)$data['id']]]></code>
@ -3053,6 +3073,11 @@
<code><![CDATA[listen]]></code>
</DeprecatedMethod>
</file>
<file src="core/Command/User/Add.php">
<DeprecatedMethod>
<code><![CDATA[validateMailAddress]]></code>
</DeprecatedMethod>
</file>
<file src="core/Command/User/AuthTokens/Add.php">
<DeprecatedClass>
<code><![CDATA[IToken::DO_NOT_REMEMBER]]></code>

View file

@ -655,6 +655,7 @@ return array(
'OCP\\Mail\\Headers\\AutoSubmitted' => $baseDir . '/lib/public/Mail/Headers/AutoSubmitted.php',
'OCP\\Mail\\IAttachment' => $baseDir . '/lib/public/Mail/IAttachment.php',
'OCP\\Mail\\IEMailTemplate' => $baseDir . '/lib/public/Mail/IEMailTemplate.php',
'OCP\\Mail\\IEmailValidator' => $baseDir . '/lib/public/Mail/IEmailValidator.php',
'OCP\\Mail\\IMailer' => $baseDir . '/lib/public/Mail/IMailer.php',
'OCP\\Mail\\IMessage' => $baseDir . '/lib/public/Mail/IMessage.php',
'OCP\\Mail\\Provider\\Address' => $baseDir . '/lib/public/Mail/Provider/Address.php',
@ -1820,6 +1821,7 @@ return array(
'OC\\Log\\Systemdlog' => $baseDir . '/lib/private/Log/Systemdlog.php',
'OC\\Mail\\Attachment' => $baseDir . '/lib/private/Mail/Attachment.php',
'OC\\Mail\\EMailTemplate' => $baseDir . '/lib/private/Mail/EMailTemplate.php',
'OC\\Mail\\EmailValidator' => $baseDir . '/lib/private/Mail/EmailValidator.php',
'OC\\Mail\\Mailer' => $baseDir . '/lib/private/Mail/Mailer.php',
'OC\\Mail\\Message' => $baseDir . '/lib/private/Mail/Message.php',
'OC\\Mail\\Provider\\Manager' => $baseDir . '/lib/private/Mail/Provider/Manager.php',

View file

@ -696,6 +696,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Mail\\Headers\\AutoSubmitted' => __DIR__ . '/../../..' . '/lib/public/Mail/Headers/AutoSubmitted.php',
'OCP\\Mail\\IAttachment' => __DIR__ . '/../../..' . '/lib/public/Mail/IAttachment.php',
'OCP\\Mail\\IEMailTemplate' => __DIR__ . '/../../..' . '/lib/public/Mail/IEMailTemplate.php',
'OCP\\Mail\\IEmailValidator' => __DIR__ . '/../../..' . '/lib/public/Mail/IEmailValidator.php',
'OCP\\Mail\\IMailer' => __DIR__ . '/../../..' . '/lib/public/Mail/IMailer.php',
'OCP\\Mail\\IMessage' => __DIR__ . '/../../..' . '/lib/public/Mail/IMessage.php',
'OCP\\Mail\\Provider\\Address' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/Address.php',
@ -1861,6 +1862,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Log\\Systemdlog' => __DIR__ . '/../../..' . '/lib/private/Log/Systemdlog.php',
'OC\\Mail\\Attachment' => __DIR__ . '/../../..' . '/lib/private/Mail/Attachment.php',
'OC\\Mail\\EMailTemplate' => __DIR__ . '/../../..' . '/lib/private/Mail/EMailTemplate.php',
'OC\\Mail\\EmailValidator' => __DIR__ . '/../../..' . '/lib/private/Mail/EmailValidator.php',
'OC\\Mail\\Mailer' => __DIR__ . '/../../..' . '/lib/private/Mail/Mailer.php',
'OC\\Mail\\Message' => __DIR__ . '/../../..' . '/lib/private/Mail/Message.php',
'OC\\Mail\\Provider\\Manager' => __DIR__ . '/../../..' . '/lib/private/Mail/Provider/Manager.php',

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Mail;
use Egulias\EmailValidator\EmailValidator as EquliasEmailValidator;
use Egulias\EmailValidator\Validation\NoRFCWarningsValidation;
use Egulias\EmailValidator\Validation\RFCValidation;
use OCP\IAppConfig;
use OCP\Mail\IEmailValidator;
class EmailValidator implements IEmailValidator {
public function __construct(
private IAppConfig $appConfig,
) {
}
public function isValid(string $email): bool {
if ($email === '') {
// Shortcut: empty addresses are never valid
return false;
}
$strictMailCheck = $this->appConfig->getValueString('core', 'enforce_strict_email_check', 'yes') === 'yes';
$validator = new EquliasEmailValidator();
$validation = $strictMailCheck ? new NoRFCWarningsValidation() : new RFCValidation();
return $validator->isValid($email, $validation);
}
}

View file

@ -8,9 +8,6 @@ declare(strict_types=1);
*/
namespace OC\Mail;
use Egulias\EmailValidator\EmailValidator;
use Egulias\EmailValidator\Validation\NoRFCWarningsValidation;
use Egulias\EmailValidator\Validation\RFCValidation;
use OCP\Defaults;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IBinaryFinder;
@ -21,6 +18,7 @@ use OCP\L10N\IFactory;
use OCP\Mail\Events\BeforeMessageSent;
use OCP\Mail\IAttachment;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IEmailValidator;
use OCP\Mail\IMailer;
use OCP\Mail\IMessage;
use Psr\Log\LoggerInterface;
@ -69,6 +67,7 @@ class Mailer implements IMailer {
private IL10N $l10n,
private IEventDispatcher $dispatcher,
private IFactory $l10nFactory,
private IEmailValidator $emailValidator,
) {
}
@ -234,18 +233,10 @@ class Mailer implements IMailer {
/**
* @param string $email Email address to be validated
* @return bool True if the mail address is valid, false otherwise
* @deprecated 26.0.0 use IEmailValidator.isValid instead
*/
public function validateMailAddress(string $email): bool {
if ($email === '') {
// Shortcut: empty addresses are never valid
return false;
}
$strictMailCheck = $this->config->getAppValue('core', 'enforce_strict_email_check', 'yes') === 'yes';
$validator = new EmailValidator();
$validation = $strictMailCheck ? new NoRFCWarningsValidation() : new RFCValidation();
return $validator->isValid($email, $validation);
return $this->emailValidator->isValid($email);
}
protected function getInstance(): MailerInterface {

View file

@ -77,6 +77,7 @@ use OC\Lock\NoopLockingProvider;
use OC\Lockdown\LockdownManager;
use OC\Log\LogFactory;
use OC\Log\PsrLoggerAdapter;
use OC\Mail\EmailValidator;
use OC\Mail\Mailer;
use OC\Memcache\ArrayCache;
use OC\Memcache\Factory;
@ -195,6 +196,7 @@ use OCP\LDAP\ILDAPProviderFactory;
use OCP\Lock\ILockingProvider;
use OCP\Lockdown\ILockdownManager;
use OCP\Log\ILogFactory;
use OCP\Mail\IEmailValidator;
use OCP\Mail\IMailer;
use OCP\OCM\ICapabilityAwareOCMProvider;
use OCP\OCM\IOCMDiscoveryService;
@ -916,6 +918,9 @@ class Server extends ServerContainer implements IServerContainer {
);
});
/** @since 32.0.0 */
$this->registerAlias(IEmailValidator::class, EmailValidator::class);
$this->registerService(IMailer::class, function (Server $c) {
return new Mailer(
$c->get(\OCP\IConfig::class),
@ -924,7 +929,8 @@ class Server extends ServerContainer implements IServerContainer {
$c->get(IURLGenerator::class),
$c->getL10N('lib'),
$c->get(IEventDispatcher::class),
$c->get(IFactory::class)
$c->get(IFactory::class),
$c->get(IEmailValidator::class),
);
});

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP\Mail;
/**
* Validator for email addresses
*
* @since 32.0.0
*/
interface IEmailValidator {
/**
* @param string $email Email address to be validated
* @return bool True if the mail address is valid, false otherwise
* @since 32.0.0
*/
public function isValid(string $email): bool;
}

View file

@ -80,6 +80,7 @@ interface IMailer {
* @param string $email Email address to be validated
* @return bool True if the mail address is valid, false otherwise
* @since 8.1.0
* @deprecated 26.0.0 use IEmailValidator.isValid instead
*/
public function validateMailAddress(string $email): bool;
}

View file

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\Mail;
use OC\Mail\EmailValidator;
use OCP\IAppConfig;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class EmailValidatorTest extends TestCase {
private IAppConfig&MockObject $appConfig;
private EmailValidator $emailValidator;
protected function setUp(): void {
parent::setUp();
$this->appConfig = $this->createMock(IAppConfig::class);
$this->emailValidator = new EmailValidator($this->appConfig);
}
public static function mailAddressProvider(): array {
return [
['lukas@nextcloud.com', true, false],
['lukas@localhost', true, false],
['lukas@192.168.1.1', true, false],
['lukas@éxämplè.com', true, false],
['asdf', false, false],
['', false, false],
['lukas@nextcloud.org@nextcloud.com', false, false],
['test@localhost', true, false],
['test@localhost', false, true],
];
}
#[DataProvider('mailAddressProvider')]
public function testIsValid($email, $expected, $strict): void {
$this->appConfig
->expects($this->atMost(1))
->method('getValueString')
->with('core', 'enforce_strict_email_check', 'yes')
->willReturn($strict ? 'yes' : 'no');
$this->assertSame($expected, $this->emailValidator->isValid($email));
}
}

View file

@ -19,6 +19,7 @@ use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\L10N\IFactory;
use OCP\Mail\Events\BeforeMessageSent;
use OCP\Mail\IEmailValidator;
use OCP\Server;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
@ -61,7 +62,8 @@ class MailerTest extends TestCase {
$this->urlGenerator,
$this->l10n,
$this->dispatcher,
$this->createMock(IFactory::class)
$this->createMock(IFactory::class),
$this->createMock(IEmailValidator::class),
);
}
@ -181,7 +183,8 @@ class MailerTest extends TestCase {
$this->urlGenerator,
$this->l10n,
$this->dispatcher,
$this->createMock(IFactory::class)
$this->createMock(IFactory::class),
$this->createMock(IEmailValidator::class),
]
)
->getMock();
@ -226,33 +229,6 @@ class MailerTest extends TestCase {
$this->mailer->send($message);
}
/**
* @return array
*/
public static function mailAddressProvider(): array {
return [
['lukas@owncloud.com', true, false],
['lukas@localhost', true, false],
['lukas@192.168.1.1', true, false],
['lukas@éxämplè.com', true, false],
['asdf', false, false],
['', false, false],
['lukas@owncloud.org@owncloud.com', false, false],
['test@localhost', true, false],
['test@localhost', false, true],
];
}
#[\PHPUnit\Framework\Attributes\DataProvider('mailAddressProvider')]
public function testValidateMailAddress($email, $expected, $strict): void {
$this->config
->expects($this->atMost(1))
->method('getAppValue')
->with('core', 'enforce_strict_email_check')
->willReturn($strict ? 'yes' : 'no');
$this->assertSame($expected, $this->mailer->validateMailAddress($email));
}
public function testCreateEMailTemplate(): void {
$this->config->method('getSystemValueString')
->with('mail_template_class', '')

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\Traits;
use OC\Mail\EmailValidator;
use OCP\IAppConfig;
use OCP\Mail\IEmailValidator;
use PHPUnit\Framework\TestCase;
trait EmailValidatorTrait {
protected function getEmailValidatorWithStrictEmailCheck(): IEmailValidator {
if (!($this instanceof TestCase)) {
throw new \RuntimeException('This trait can only be used in a test case');
}
$appConfig = $this->createMock(IAppConfig::class);
$appConfig->method('getValueString')
->with('core', 'enforce_strict_email_check', 'yes')
->willReturn('yes');
return new EmailValidator($appConfig);
}
}