Skip to content

Commit 451cab5

Browse files
committed
feat(UserMigration): Overwork migration to include all settings (trusted senders)
Signed-off-by: David Dreschner <david.dreschner@nextcloud.com>
1 parent 00fcc54 commit 451cab5

6 files changed

Lines changed: 270 additions & 0 deletions

File tree

lib/Contracts/ITrustedSenderService.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ public function trust(string $uid, string $email, string $type, ?bool $trust = t
2121
* @return TrustedSender[]
2222
*/
2323
public function getTrusted(string $uid): array;
24+
25+
public function removeTrusted(string $uid): void;
2426
}

lib/Db/TrustedSenderMapper.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ public function remove(string $uid, string $email, string $type): void {
7272
$delete->executeStatement();
7373
}
7474

75+
public function removeAll(string $uid): void {
76+
$qb = $this->db->getQueryBuilder();
77+
$delete = $qb->delete($this->getTableName())
78+
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($uid)));
79+
$delete->executeStatement();
80+
}
81+
7582
/**
7683
* @param string $uid
7784
* @return TrustedSender[]

lib/Service/TrustedSenderService.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,8 @@ public function trust(string $uid, string $email, string $type, ?bool $trust = t
7272
public function getTrusted(string $uid): array {
7373
return $this->mapper->findAll($uid);
7474
}
75+
76+
public function removeTrusted(string $uid): void {
77+
$this->mapper->removeAll($uid);
78+
}
7579
}

lib/UserMigration/MailAccountMigrator.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use OCA\Mail\Exception\ServiceException;
1515
use OCA\Mail\UserMigration\Service\AccountMigrationService;
1616
use OCA\Mail\UserMigration\Service\AppConfigMigrationService;
17+
use OCA\Mail\UserMigration\Service\TrustedSendersMigrationService;
1718
use OCP\AppFramework\Db\DoesNotExistException;
1819
use OCP\IL10N;
1920
use OCP\IUser;
@@ -33,6 +34,7 @@ public function __construct(
3334
private readonly ICrypto $crypto,
3435
private readonly AccountMigrationService $accountMigrationService,
3536
private readonly AppConfigMigrationService $appConfigMigrationService,
37+
private readonly TrustedSendersMigrationService $trustedSendersMigrationService,
3638
) {
3739
}
3840

@@ -44,6 +46,7 @@ public function export(IUser $user,
4446
$output->writeln($this->l10n->t("Exporting mail accounts for user {$user->getUID()}"), OutputInterface::VERBOSITY_VERBOSE);
4547

4648
$this->appConfigMigrationService->exportAppConfiguration($user, $exportDestination, $output);
49+
$this->trustedSendersMigrationService->exportTrustedSenders($user, $exportDestination, $output);
4750
}
4851

4952
#[\Override]
@@ -52,6 +55,7 @@ public function import(IUser $user, IImportSource $importSource, OutputInterface
5255
$this->deleteExistingData($user, $output);
5356

5457
$this->appConfigMigrationService->importAppConfiguration($user, $importSource, $output);
58+
$this->trustedSendersMigrationService->importTrustedSenders($user, $importSource, $output);
5559

5660
$this->accountMigrationService->scheduleBackgroundJobs($user, $output);
5761
}
@@ -70,6 +74,7 @@ private function deleteExistingData(IUser $user, OutputInterface $output): void
7074
$output->writeln($this->l10n->t("Deleting existing mail data for user {$user->getUID()}"), OutputInterface::VERBOSITY_VERBOSE);
7175

7276
$this->appConfigMigrationService->deleteAppConfiguration($user, $output);
77+
$this->trustedSendersMigrationService->removeAllTrustedSenders($user, $output);
7378
$this->accountMigrationService->deleteAllAccounts($user, $output);
7479
}
7580

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-only
8+
*/
9+
10+
namespace OCA\Mail\UserMigration\Service;
11+
12+
use JsonException;
13+
use OCA\Mail\Contracts\ITrustedSenderService;
14+
use OCA\Mail\UserMigration\MailAccountMigrator;
15+
use OCP\IL10N;
16+
use OCP\IUser;
17+
use OCP\UserMigration\IExportDestination;
18+
use OCP\UserMigration\IImportSource;
19+
use OCP\UserMigration\UserMigrationException;
20+
use Symfony\Component\Console\Output\OutputInterface;
21+
22+
class TrustedSendersMigrationService {
23+
public const TRUSTED_SENDERS_FILE = MailAccountMigrator::EXPORT_ROOT . '/trusted_senders.json';
24+
25+
public function __construct(
26+
private readonly ITrustedSenderService $trustedSenderService,
27+
private readonly IL10N $l10n,
28+
) {
29+
}
30+
31+
/**
32+
* Export all addresses the user defined as trustworthy.
33+
*
34+
* @throws UserMigrationException
35+
*/
36+
public function exportTrustedSenders(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void {
37+
$output->writeln($this->l10n->t('Exporting trusted senders for user %s', $user->getUID()), OutputInterface::VERBOSITY_VERBOSE);
38+
39+
$trustedSenders = $this->trustedSenderService->getTrusted($user->getUID());
40+
41+
try {
42+
$exportDestination->addFileContents(self::TRUSTED_SENDERS_FILE, json_encode($trustedSenders, JSON_THROW_ON_ERROR));
43+
} catch (JsonException|UserMigrationException) {
44+
throw new UserMigrationException("Failed to export mail app configuration for user {$user->getUID()}");
45+
}
46+
}
47+
48+
/**
49+
* Import all addresses the user defined as trustworthy
50+
* on export.
51+
*
52+
* @throws UserMigrationException
53+
*/
54+
public function importTrustedSenders(IUser $user, IImportSource $importSource, OutputInterface $output): void {
55+
$output->writeln(
56+
$this->l10n->t('Importing trusted senders for user %s', $user->getUID()),
57+
OutputInterface::VERBOSITY_VERBOSE
58+
);
59+
60+
try {
61+
$trustedSendersFileContent = $importSource->getFileContents(self::TRUSTED_SENDERS_FILE);
62+
} catch (UserMigrationException) {
63+
$output->writeln(
64+
$this->l10n->t('Trusted senders configuration for user %s not found. Continue...', $user->getUID()),
65+
OutputInterface::VERBOSITY_VERBOSE
66+
);
67+
68+
return;
69+
}
70+
71+
$trustedSenders = json_decode($trustedSendersFileContent, true);
72+
$this->validateTrustedSenders($trustedSenders);
73+
74+
foreach ($trustedSenders as $trustedSender) {
75+
$output->writeln(
76+
$this->l10n->t('Importing trusted sender %s for user %s', [$trustedSender['email'], $user->getUID()]),
77+
OutputInterface::VERBOSITY_VERBOSE
78+
);
79+
80+
$this->trustedSenderService->trust($user->getUID(), $trustedSender['email'], $trustedSender['type']);
81+
}
82+
}
83+
84+
public function removeAllTrustedSenders(IUser $user, OutputInterface $output): void {
85+
$output->writeln(
86+
$this->l10n->t('Delete existing trusted senders for user %s', $user->getUID()),
87+
OutputInterface::VERBOSITY_VERBOSE
88+
);
89+
90+
$this->trustedSenderService->removeTrusted($user->getUID());
91+
}
92+
93+
/**
94+
* Validate the parsed trusted senders to ensure they
95+
* have the expected structure and types.
96+
*
97+
* @throws UserMigrationException
98+
*/
99+
private function validateTrustedSenders(mixed $trustedSenders): void {
100+
$trustedSendersArrayIsValid = is_array($trustedSenders) && array_is_list($trustedSenders);
101+
if (!$trustedSendersArrayIsValid) {
102+
throw new UserMigrationException('Invalid trusted senders export structure');
103+
}
104+
105+
foreach ($trustedSenders as $trustedSender) {
106+
$trustedSenderArrayIsValid = is_array($trustedSender);
107+
108+
$emailIsValid = $trustedSenderArrayIsValid
109+
&& array_key_exists('email', $trustedSender)
110+
&& is_string($trustedSender['email']);
111+
112+
$typeIsValid = $trustedSenderArrayIsValid
113+
&& array_key_exists('type', $trustedSender)
114+
&& is_string($trustedSender['type']);
115+
116+
if (!$emailIsValid || !$typeIsValid) {
117+
throw new UserMigrationException('Invalid trusted sender entry');
118+
}
119+
}
120+
}
121+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-only
8+
*/
9+
10+
namespace Unit\UserMigration\Service;
11+
12+
use ChristophWurst\Nextcloud\Testing\ServiceMockObject;
13+
use ChristophWurst\Nextcloud\Testing\TestCase;
14+
use OCA\Mail\Db\TrustedSender;
15+
use OCA\Mail\UserMigration\Service\TrustedSendersMigrationService;
16+
use OCP\IUser;
17+
use OCP\UserMigration\IExportDestination;
18+
use OCP\UserMigration\IImportSource;
19+
use OCP\UserMigration\UserMigrationException;
20+
use Symfony\Component\Console\Output\OutputInterface;
21+
22+
class TrustedSendersMigrationServiceTest extends TestCase {
23+
private const USER_ID = '123';
24+
private OutputInterface $output;
25+
private IUser $user;
26+
private IExportDestination $exportDestination;
27+
private IImportSource $importSource;
28+
private ServiceMockObject $serviceMock;
29+
private TrustedSendersMigrationService $migrationService;
30+
31+
protected function setUp(): void {
32+
parent::setUp();
33+
34+
$this->output = $this->createMock(OutputInterface::class);
35+
$this->exportDestination = $this->createMock(IExportDestination::class);
36+
$this->importSource = $this->createMock(IImportSource::class);
37+
38+
$this->user = $this->createMock(IUser::class);
39+
$this->user->method('getUID')->willReturn(self::USER_ID);
40+
41+
$this->serviceMock = $this->createServiceMock(TrustedSendersMigrationService::class);
42+
$this->migrationService = $this->serviceMock->getService();
43+
}
44+
45+
public function testExportsMultipleTrustedSenders(): void {
46+
$trustedSendersList = [$this->getTrustedIndividual(), $this->getTrustedDomain()];
47+
$this->exportDestination->expects(self::once())->method('addFileContents')->with(TrustedSendersMigrationService::TRUSTED_SENDERS_FILE, json_encode($trustedSendersList));
48+
49+
$this->serviceMock->getParameter('trustedSenderService')->method('getTrusted')->with(self::USER_ID)->willReturn($trustedSendersList);
50+
$this->migrationService->exportTrustedSenders($this->user, $this->exportDestination, $this->output);
51+
}
52+
53+
public function testExportsNoneTrustedSenders(): void {
54+
$trustedSendersList = [];
55+
$this->exportDestination->expects(self::once())->method('addFileContents')->with(TrustedSendersMigrationService::TRUSTED_SENDERS_FILE, json_encode($trustedSendersList));
56+
57+
$this->serviceMock->getParameter('trustedSenderService')->method('getTrusted')->with(self::USER_ID)->willReturn($trustedSendersList);
58+
$this->migrationService->exportTrustedSenders($this->user, $this->exportDestination, $this->output);
59+
}
60+
61+
public function testImportMultipleTrustedSenders(): void {
62+
$trustedIndividual = $this->getTrustedIndividual();
63+
$trustedDomain = $this->getTrustedDomain();
64+
$trustedSendersList = [$trustedIndividual, $trustedDomain];
65+
$this->importSource->expects(self::once())->method('getFileContents')->with(TrustedSendersMigrationService::TRUSTED_SENDERS_FILE)->willReturn(json_encode($trustedSendersList));
66+
67+
$this->serviceMock->getParameter('trustedSenderService')->expects(self::exactly(2))->method('trust')->with(self::USER_ID, self::callback(function ($email) use ($trustedIndividual, $trustedDomain) {
68+
return $email === $trustedIndividual->getEmail() || $email === $trustedDomain->getEmail();
69+
}), self::callback(function ($type) use ($trustedIndividual, $trustedDomain) {
70+
return $type === $trustedIndividual->getType() || $type === $trustedDomain->getType();
71+
}));
72+
73+
$this->migrationService->importTrustedSenders($this->user, $this->importSource, $this->output);
74+
}
75+
76+
public function testImportNoneTrustedSenders(): void {
77+
$trustedSendersList = [];
78+
$this->importSource->expects(self::once())->method('getFileContents')->with(TrustedSendersMigrationService::TRUSTED_SENDERS_FILE)->willReturn(json_encode($trustedSendersList));
79+
$this->serviceMock->getParameter('trustedSenderService')->expects(self::never())->method('trust');
80+
$this->migrationService->importTrustedSenders($this->user, $this->importSource, $this->output);
81+
}
82+
83+
public function testImportInvalidJsonThrowsException(): void {
84+
$this->importSource->expects(self::once())->method('getFileContents')->with(TrustedSendersMigrationService::TRUSTED_SENDERS_FILE)->willReturn('this is not valid json {[}');
85+
86+
$this->serviceMock->getParameter('trustedSenderService')->expects(self::never())->method('trust');
87+
$this->expectException(UserMigrationException::class);
88+
89+
$this->migrationService->importTrustedSenders($this->user, $this->importSource, $this->output);
90+
}
91+
92+
public function testImportEmptyStringThrowsException(): void {
93+
$this->importSource->expects(self::once())->method('getFileContents')->with(TrustedSendersMigrationService::TRUSTED_SENDERS_FILE)->willReturn('');
94+
95+
$this->serviceMock->getParameter('trustedSenderService')->expects(self::never())->method('trust');
96+
$this->expectException(UserMigrationException::class);
97+
98+
$this->migrationService->importTrustedSenders($this->user, $this->importSource, $this->output);
99+
}
100+
101+
public function testImportJsonWithNonArrayRootThrowsException(): void {
102+
$this->importSource->expects(self::once())->method('getFileContents')->with(TrustedSendersMigrationService::TRUSTED_SENDERS_FILE)->willReturn('"just a string"');
103+
104+
$this->serviceMock->getParameter('trustedSenderService')->expects(self::never())->method('trust');
105+
$this->expectException(UserMigrationException::class);
106+
107+
$this->migrationService->importTrustedSenders($this->user, $this->importSource, $this->output);
108+
}
109+
110+
private function getTrustedIndividual(): TrustedSender {
111+
$individualSender = new TrustedSender;
112+
113+
$individualSender->setId(1);
114+
$individualSender->setUserId(self::USER_ID);
115+
$individualSender->setEmail('max@mustermann.com');
116+
$individualSender->setType('individual');
117+
118+
return $individualSender;
119+
}
120+
121+
private function getTrustedDomain(): TrustedSender {
122+
$domainSender = new TrustedSender();
123+
124+
$domainSender->setId(2);
125+
$domainSender->setUserId(self::USER_ID);
126+
$domainSender->setEmail('nextcloud.com');
127+
$domainSender->setType('domain');
128+
129+
return $domainSender;
130+
}
131+
}

0 commit comments

Comments
 (0)