-
Notifications
You must be signed in to change notification settings - Fork 272
fix: normalize email addresses when retrieving from server #11055
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,14 +29,21 @@ private function __construct(Horde_Mail_Rfc822_Address $wrapped) { | |
$this->wrapped = $wrapped; | ||
} | ||
|
||
public static function fromHorde(Horde_Mail_Rfc822_Address $horde): self { | ||
public static function fromHorde(Horde_Mail_Rfc822_Address $horde, bool $normalize = false): self { | ||
if ($normalize) { | ||
return self::fromRaw($horde->personal, $horde->bare_address, $normalize); | ||
} | ||
return new self($horde); | ||
} | ||
|
||
public static function fromRaw(string $label, string $email): self { | ||
$wrapped = new Horde_Mail_Rfc822_Address($email); | ||
public static function fromRaw(?string $label, string $email, bool $normalize = false): self { | ||
if ($normalize) { | ||
$wrapped = new Horde_Mail_Rfc822_Address(self::normalizeAddress($email)); | ||
} else { | ||
$wrapped = new Horde_Mail_Rfc822_Address($email); | ||
} | ||
// If no label is set we use the email | ||
if ($label !== $email) { | ||
if ($label !== null && $label !== $email) { | ||
$wrapped->personal = $label; | ||
} | ||
return new self($wrapped); | ||
|
@@ -117,4 +124,13 @@ public function equals($object): bool { | |
return $this->getEmail() === $object->getEmail() | ||
&& $this->getLabel() === $object->getLabel(); | ||
} | ||
|
||
private static function normalizeAddress(string $address): string { | ||
// remove single quotes and whitespace the might exist | ||
// Examples: | ||
// [email protected] | ||
// '[email protected]' | ||
// ' [email protected]' | ||
return strtolower(trim(trim($address, "'"))); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,58 @@ | ||||||||||
<?php | ||||||||||
|
||||||||||
declare(strict_types=1); | ||||||||||
|
||||||||||
/** | ||||||||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors | ||||||||||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||||||||||
*/ | ||||||||||
|
||||||||||
namespace OCA\Mail\BackgroundJob; | ||||||||||
|
||||||||||
use OCP\AppFramework\Utility\ITimeFactory; | ||||||||||
use OCP\BackgroundJob\IJobList; | ||||||||||
use OCP\BackgroundJob\TimedJob; | ||||||||||
use OCP\DB\QueryBuilder\IQueryBuilder; | ||||||||||
use OCP\IDBConnection; | ||||||||||
|
||||||||||
class RepairRecipients extends TimedJob { | ||||||||||
|
||||||||||
public function __construct( | ||||||||||
protected ITimeFactory $time, | ||||||||||
private IDBConnection $db, | ||||||||||
private IJobList $jobService, | ||||||||||
) { | ||||||||||
parent::__construct($time); | ||||||||||
$this->setInterval(300); | ||||||||||
} | ||||||||||
|
||||||||||
protected function run($argument): void { | ||||||||||
// fetch all quoted emails | ||||||||||
$select = $this->db->getQueryBuilder(); | ||||||||||
$select->select('id', 'email') | ||||||||||
->from('mail_recipients') | ||||||||||
->where( | ||||||||||
$select->expr()->like('email', $select->createNamedParameter('\'%\'', IQueryBuilder::PARAM_STR)) | ||||||||||
) | ||||||||||
->setMaxResults(1000); | ||||||||||
$recipients = $select->executeQuery()->fetchAll(); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
// update emails | ||||||||||
$update = $this->db->getQueryBuilder(); | ||||||||||
$update->update('mail_recipients') | ||||||||||
->set('email', $update->createParameter('email')) | ||||||||||
->where($update->expr()->in('id', $update->createParameter('id'), IQueryBuilder::PARAM_STR)); | ||||||||||
foreach ($recipients as $recipient) { | ||||||||||
$id = $recipient['id']; | ||||||||||
$email = $recipient['email']; | ||||||||||
$email = trim(str_replace('\'', '', (string)$email)); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
The best approach would be to reuse the logic from the address class. If we don't go with the helper/utility, we should still use trim to remove the first and last character (even if we can be very sure, due to the query, that we're only dealing with emails starting and ending with a |
||||||||||
$update->setParameter('id', $id, IQueryBuilder::PARAM_STR); | ||||||||||
$update->setParameter('email', $email, IQueryBuilder::PARAM_STR); | ||||||||||
$update->executeStatement(); | ||||||||||
} | ||||||||||
// remove job depending on the result | ||||||||||
if ($recipients === []) { | ||||||||||
$this->jobService->remove(RepairRecipients::class); | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/** | ||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
|
||
namespace OCA\Mail\Migration; | ||
|
||
use Closure; | ||
use OCP\BackgroundJob\IJobList; | ||
use OCP\Migration\IOutput; | ||
use OCP\Migration\SimpleMigrationStep; | ||
|
||
class Version5000Date20250507000000 extends SimpleMigrationStep { | ||
|
||
public function __construct( | ||
private IJobList $jobService, | ||
) { | ||
} | ||
|
||
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options) { | ||
$this->jobService->add(\OCA\Mail\BackgroundJob\RepairRecipients::class); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -68,4 +68,36 @@ public function testDoesNotEqualBecauseDifferentLabel() { | |
|
||
$this->assertFalse($equals); | ||
} | ||
|
||
public function testNormalizedWithSingleQuotes() { | ||
$address = Address::fromRaw(null, "'[email protected]'", true)->toHorde(); | ||
$this->assertEquals('[email protected]', $address->bare_address); | ||
|
||
$address = Address::fromHorde(new Horde_Mail_Rfc822_Address("'[email protected]'"), true)->toHorde(); | ||
$this->assertEquals('[email protected]', $address->bare_address); | ||
} | ||
|
||
public function testUnnormalizedWithSingleQuotes() { | ||
$address = Address::fromRaw(null, "'[email protected]'", false)->toHorde(); | ||
$this->assertEquals("'[email protected]'", $address->bare_address); | ||
|
||
$address = Address::fromHorde(new Horde_Mail_Rfc822_Address("'[email protected]'"), false)->toHorde(); | ||
$this->assertEquals("'[email protected]'", $address->bare_address); | ||
} | ||
|
||
public function testNormalizedWithUpperCaseLetters() { | ||
$address = Address::fromRaw(null, '[email protected]', true)->toHorde(); | ||
$this->assertEquals('[email protected]', $address->bare_address); | ||
|
||
$address = Address::fromHorde(new Horde_Mail_Rfc822_Address('[email protected]'), true)->toHorde(); | ||
$this->assertEquals('[email protected]', $address->bare_address); | ||
} | ||
|
||
public function testUnnormalizedWithUpperCaseLetters() { | ||
$address = Address::fromRaw(null, '[email protected]', false)->toHorde(); | ||
$this->assertEquals('[email protected]', $address->bare_address); | ||
|
||
$address = Address::fromHorde(new Horde_Mail_Rfc822_Address('[email protected]'), false)->toHorde(); | ||
$this->assertEquals('[email protected]', $address->bare_address); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned at #10980 (comment).
'[email protected]
is a valid email address.[email protected]
Example:
Given that we need the same logic in the repair job, I'd suggest moving the normalization to a small helper or utility service to make it reusable and more testable.