Skip to content

Commit 1c6baa0

Browse files
committed
feat(UserMigration): Overwork migration to include all settings (internal addresses)
Signed-off-by: David Dreschner <david.dreschner@nextcloud.com>
1 parent bf260fd commit 1c6baa0

3 files changed

Lines changed: 238 additions & 0 deletions

File tree

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\InternalAddressesMigrationService;
1718
use OCA\Mail\UserMigration\Service\SMIMEMigrationService;
1819
use OCA\Mail\UserMigration\Service\TagsMigrationService;
1920
use OCA\Mail\UserMigration\Service\TextBlocksMigrationService;
@@ -37,6 +38,7 @@ public function __construct(
3738
private readonly ICrypto $crypto,
3839
private readonly AccountMigrationService $accountMigrationService,
3940
private readonly AppConfigMigrationService $appConfigMigrationService,
41+
private readonly InternalAddressesMigrationService $internalAddressesMigrationService,
4042
private readonly TrustedSendersMigrationService $trustedSendersMigrationService,
4143
private readonly TextBlocksMigrationService $textBlocksMigrationService,
4244
private readonly TagsMigrationService $tagsMigrationService,
@@ -52,6 +54,7 @@ public function export(IUser $user,
5254
$output->writeln($this->l10n->t("Exporting mail accounts for user {$user->getUID()}"), OutputInterface::VERBOSITY_VERBOSE);
5355

5456
$this->appConfigMigrationService->exportAppConfiguration($user, $exportDestination, $output);
57+
$this->internalAddressesMigrationService->exportInternalAddresses($user, $exportDestination, $output);
5558
$this->trustedSendersMigrationService->exportTrustedSenders($user, $exportDestination, $output);
5659
$this->textBlocksMigrationService->exportTextBlocks($user, $exportDestination, $output);
5760
$this->tagsMigrationService->exportTags($user, $exportDestination, $output);
@@ -65,6 +68,7 @@ public function import(IUser $user, IImportSource $importSource, OutputInterface
6568
$this->deleteExistingData($user, $output);
6669

6770
$this->appConfigMigrationService->importAppConfiguration($user, $importSource, $output);
71+
$this->internalAddressesMigrationService->importInternalAddresses($user, $importSource, $output);
6872
$this->trustedSendersMigrationService->importTrustedSenders($user, $importSource, $output);
6973
$this->textBlocksMigrationService->importTextBlocks($user, $importSource, $output);
7074
$newTagIds = $this->tagsMigrationService->importTags($user, $importSource, $output);
@@ -87,6 +91,7 @@ private function deleteExistingData(IUser $user, OutputInterface $output): void
8791
$output->writeln($this->l10n->t("Deleting existing mail data for user {$user->getUID()}"), OutputInterface::VERBOSITY_VERBOSE);
8892

8993
$this->appConfigMigrationService->deleteAppConfiguration($user, $output);
94+
$this->internalAddressesMigrationService->removeInternalAddresses($user, $output);
9095
$this->trustedSendersMigrationService->removeAllTrustedSenders($user, $output);
9196
$this->textBlocksMigrationService->deleteAllTextBlocks($user, $output);
9297
$this->tagsMigrationService->deleteAllTags($user, $output);
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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-or-later
8+
*/
9+
10+
namespace OCA\Mail\UserMigration\Service;
11+
12+
use JsonException;
13+
use OCA\Mail\Contracts\IInternalAddressService;
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 InternalAddressesMigrationService {
23+
public const INTERNAL_ADDRESSES_FILE = MailAccountMigrator::EXPORT_ROOT . '/internal_addresses.json';
24+
25+
public function __construct(
26+
private readonly IInternalAddressService $internalAddressService,
27+
private readonly IL10N $l10n,
28+
) {
29+
}
30+
31+
/**
32+
* Export all addresses the user defined as internal ones
33+
* on export.
34+
*
35+
* @param IUser $user
36+
* @param IExportDestination $exportDestination
37+
* @param OutputInterface $output
38+
* @throws UserMigrationException
39+
*/
40+
public function exportInternalAddresses(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void {
41+
$output->writeln(
42+
$this->l10n->t('Exporting internal addresses for user %s', [$user->getUID()]),
43+
OutputInterface::VERBOSITY_VERBOSE
44+
);
45+
46+
$internalAddresses = $this->internalAddressService->getInternalAddresses($user->getUID());
47+
48+
try {
49+
$exportDestination->addFileContents(self::INTERNAL_ADDRESSES_FILE, json_encode($internalAddresses, JSON_THROW_ON_ERROR));
50+
} catch (JsonException|UserMigrationException $exception) {
51+
throw new UserMigrationException(
52+
"Failed to export internal addresses for user {$user->getUID()}",
53+
previous: $exception
54+
);
55+
}
56+
}
57+
58+
/**
59+
* Import all addresses the user defined as internal ones.
60+
*
61+
* @throws UserMigrationException
62+
*/
63+
public function importInternalAddresses(IUser $user, IImportSource $importSource, OutputInterface $output): void {
64+
$output->writeln(
65+
$this->l10n->t('Importing internal addresses for user %s', [$user->getUID()]),
66+
OutputInterface::VERBOSITY_VERBOSE
67+
);
68+
69+
$internalAddresses = json_decode($importSource->getFileContents(self::INTERNAL_ADDRESSES_FILE), true);
70+
$this->validateInternalAddresses($internalAddresses);
71+
72+
foreach ($internalAddresses as $internalAddress) {
73+
$this->internalAddressService->add($user->getUID(), $internalAddress['address'], $internalAddress['type']);
74+
}
75+
}
76+
77+
public function removeInternalAddresses(IUser $user, OutputInterface $output): void {
78+
$output->writeln(
79+
$this->l10n->t('Deleting all internal addresses for user %s', [$user->getUID()]),
80+
OutputInterface::VERBOSITY_VERBOSE
81+
);
82+
83+
$this->internalAddressService->removeInternalAddresses($user->getUID());
84+
}
85+
86+
/**
87+
* Validate the parsed internal addresses to ensure they
88+
* have the expected structure and types.
89+
*
90+
* @throws UserMigrationException
91+
*/
92+
private function validateInternalAddresses(mixed $internalAddresses): void {
93+
$internalAddressesArrayIsValid = is_array($internalAddresses) && array_is_list($internalAddresses);
94+
if (!$internalAddressesArrayIsValid) {
95+
throw new UserMigrationException('Invalid internal addresses export structure');
96+
}
97+
98+
foreach ($internalAddresses as $internalAddress) {
99+
$internalAddressArrayIsValid = is_array($internalAddress);
100+
101+
$idIsValid = $internalAddressArrayIsValid
102+
&& array_key_exists('id', $internalAddress)
103+
&& is_int($internalAddress['id']);
104+
105+
$addressIsValid = $internalAddressArrayIsValid
106+
&& array_key_exists('address', $internalAddress)
107+
&& is_string($internalAddress['address']);
108+
109+
$uidIsValid = $internalAddressArrayIsValid
110+
&& array_key_exists('uid', $internalAddress)
111+
&& is_string($internalAddress['uid']);
112+
113+
$typeIsValid = $internalAddressArrayIsValid
114+
&& array_key_exists('type', $internalAddress)
115+
&& is_string($internalAddress['type']);
116+
117+
if (
118+
!$idIsValid
119+
|| !$addressIsValid
120+
|| !$uidIsValid
121+
|| !$typeIsValid
122+
) {
123+
throw new UserMigrationException('Invalid internal address entry');
124+
}
125+
}
126+
}
127+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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-or-later
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\InternalAddress;
15+
use OCA\Mail\UserMigration\Service\InternalAddressesMigrationService;
16+
use OCP\IUser;
17+
use OCP\UserMigration\IExportDestination;
18+
use OCP\UserMigration\IImportSource;
19+
use Symfony\Component\Console\Output\OutputInterface;
20+
21+
class InternalAddressesMigrationServiceTest extends TestCase {
22+
private const USER_ID = '123';
23+
private OutputInterface $output;
24+
private IUser $user;
25+
private IExportDestination $exportDestination;
26+
private IImportSource $importSource;
27+
private ServiceMockObject $serviceMock;
28+
private InternalAddressesMigrationService $migrationService;
29+
30+
protected function setUp(): void {
31+
parent::setUp();
32+
33+
$this->serviceMock = $this->createServiceMock(InternalAddressesMigrationService::class);
34+
$this->migrationService = $this->serviceMock->getService();
35+
36+
$this->user = $this->createMock(IUser::class);
37+
$this->user->method('getUID')->willReturn(self::USER_ID);
38+
39+
$this->output = $this->createMock(OutputInterface::class);
40+
$this->exportDestination = $this->createMock(IExportDestination::class);
41+
$this->importSource = $this->createMock(IImportSource::class);
42+
}
43+
44+
public function testExportsMultipleInternalAddresses(): void {
45+
$trustedSendersList = [$this->getTrustedIndividual(), $this->getTrustedDomain()];
46+
$this->exportDestination->expects(self::once())->method('addFileContents')->with(InternalAddressesMigrationService::INTERNAL_ADDRESSES_FILE, json_encode($trustedSendersList));
47+
48+
$this->serviceMock->getParameter('internalAddressService')->method('getInternalAddresses')->with(self::USER_ID)->willReturn($trustedSendersList);
49+
50+
$this->migrationService->exportInternalAddresses($this->user, $this->exportDestination, $this->output);
51+
}
52+
53+
public function testExportsNoneInternalAddress(): void {
54+
$trustedSendersList = [];
55+
$this->exportDestination->expects(self::once())->method('addFileContents')->with(InternalAddressesMigrationService::INTERNAL_ADDRESSES_FILE, json_encode($trustedSendersList));
56+
57+
$this->serviceMock->getParameter('internalAddressService')->method('getInternalAddresses')->with(self::USER_ID)->willReturn($trustedSendersList);
58+
59+
$this->migrationService->exportInternalAddresses($this->user, $this->exportDestination, $this->output);
60+
}
61+
62+
public function testImportMultipleInternalAddresses(): void {
63+
$trustedIndividual = $this->getTrustedIndividual();
64+
$trustedDomain = $this->getTrustedDomain();
65+
$trustedSendersList = [$trustedIndividual, $trustedDomain];
66+
$this->importSource->expects(self::once())->method('getFileContents')->with(InternalAddressesMigrationService::INTERNAL_ADDRESSES_FILE)->willReturn(json_encode($trustedSendersList));
67+
68+
$this->serviceMock->getParameter('internalAddressService')->expects(self::exactly(2))->method('add')->with(self::USER_ID, self::callback(function ($email) use ($trustedIndividual, $trustedDomain) {
69+
return $email === $trustedIndividual->getAddress() || $email === $trustedDomain->getAddress();
70+
}), self::callback(function ($type) use ($trustedIndividual, $trustedDomain) {
71+
return $type === $trustedIndividual->getType() || $type === $trustedDomain->getType();
72+
}));
73+
74+
$this->migrationService->importInternalAddresses($this->user, $this->importSource, $this->output);
75+
}
76+
77+
public function testImportNoneInternalAddress(): void {
78+
$trustedSendersList = [];
79+
$this->importSource->expects(self::once())->method('getFileContents')->with(InternalAddressesMigrationService::INTERNAL_ADDRESSES_FILE)->willReturn(json_encode($trustedSendersList));
80+
$this->serviceMock->getParameter('internalAddressService')->expects(self::never())->method('add');
81+
82+
$this->migrationService->importInternalAddresses($this->user, $this->importSource, $this->output);
83+
}
84+
85+
private function getTrustedIndividual(): InternalAddress {
86+
$individualSender = new InternalAddress;
87+
88+
$individualSender->setId(1);
89+
$individualSender->setUserId(self::USER_ID);
90+
$individualSender->setAddress('max@mustermann.com');
91+
$individualSender->setType('individual');
92+
93+
return $individualSender;
94+
}
95+
96+
private function getTrustedDomain(): InternalAddress {
97+
$domainSender = new InternalAddress();
98+
99+
$domainSender->setId(2);
100+
$domainSender->setUserId(self::USER_ID);
101+
$domainSender->setAddress('nextcloud.com');
102+
$domainSender->setType('domain');
103+
104+
return $domainSender;
105+
}
106+
}

0 commit comments

Comments
 (0)