Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use OCA\Mail\Contracts\IAvatarService;
use OCA\Mail\Contracts\IDkimService;
use OCA\Mail\Contracts\IDkimValidator;
use OCA\Mail\Contracts\IInternalAddressService;
use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IMailSearch;
use OCA\Mail\Contracts\IMailTransmission;
Expand Down Expand Up @@ -62,6 +63,7 @@
use OCA\Mail\Service\AvatarService;
use OCA\Mail\Service\DkimService;
use OCA\Mail\Service\DkimValidator;
use OCA\Mail\Service\InternalAddressService;
use OCA\Mail\Service\MailManager;
use OCA\Mail\Service\MailTransmission;
use OCA\Mail\Service\Search\MailSearch;
Expand Down Expand Up @@ -123,6 +125,7 @@ public function register(IRegistrationContext $context): void {
$context->registerServiceAlias(IMailManager::class, MailManager::class);
$context->registerServiceAlias(IMailSearch::class, MailSearch::class);
$context->registerServiceAlias(IMailTransmission::class, MailTransmission::class);
$context->registerServiceAlias(IInternalAddressService::class, InternalAddressService::class);
$context->registerServiceAlias(ITrustedSenderService::class, TrustedSenderService::class);
$context->registerServiceAlias(IUserPreferences::class, UserPreferenceService::class);
$context->registerServiceAlias(IDkimService::class, DkimService::class);
Expand Down
2 changes: 2 additions & 0 deletions lib/Contracts/IInternalAddressService.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public function isInternal(string $uid, string $address): bool;

public function add(string $uid, string $address, string $type, ?bool $trust = true);

public function removeInternalAddresses(string $uid): void;

/**
* @param string $uid
* @return InternalAddress[]
Expand Down
11 changes: 11 additions & 0 deletions lib/Db/InternalAddressMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\Exception;
use OCP\IDBConnection;

/**
Expand Down Expand Up @@ -100,4 +101,14 @@ public function find(string $uid, string $address): ?InternalAddress {
return null;
}
}

/**
* @throws Exception
*/
public function removeAll(string $uid) : void {
$qb = $this->db->getQueryBuilder();
$delete = $qb->delete($this->getTableName())
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($uid)));
$delete->executeStatement();
}
}
5 changes: 5 additions & 0 deletions lib/Service/InternalAddressService.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ public function add(string $uid, string $address, string $type, ?bool $trust = t
return null;
}

#[\Override]
public function removeInternalAddresses(string $uid): void {
$this->mapper->removeAll($uid);
}

#[\Override]
public function getInternalAddresses(string $uid): array {
return $this->mapper->findAll($uid);
Expand Down
5 changes: 5 additions & 0 deletions lib/UserMigration/MailAccountMigrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\UserMigration\Service\AccountMigrationService;
use OCA\Mail\UserMigration\Service\AppConfigMigrationService;
use OCA\Mail\UserMigration\Service\InternalAddressesMigrationService;
use OCA\Mail\UserMigration\Service\SMIMEMigrationService;
use OCA\Mail\UserMigration\Service\TagsMigrationService;
use OCA\Mail\UserMigration\Service\TextBlocksMigrationService;
Expand All @@ -37,6 +38,7 @@ public function __construct(
private readonly ICrypto $crypto,
private readonly AccountMigrationService $accountMigrationService,
private readonly AppConfigMigrationService $appConfigMigrationService,
private readonly InternalAddressesMigrationService $internalAddressesMigrationService,
private readonly TrustedSendersMigrationService $trustedSendersMigrationService,
private readonly TextBlocksMigrationService $textBlocksMigrationService,
private readonly TagsMigrationService $tagsMigrationService,
Expand All @@ -52,6 +54,7 @@ public function export(IUser $user,
$output->writeln($this->l10n->t("Exporting mail accounts for user {$user->getUID()}"), OutputInterface::VERBOSITY_VERBOSE);

$this->appConfigMigrationService->exportAppConfiguration($user, $exportDestination, $output);
$this->internalAddressesMigrationService->exportInternalAddresses($user, $exportDestination, $output);
$this->trustedSendersMigrationService->exportTrustedSenders($user, $exportDestination, $output);
$this->textBlocksMigrationService->exportTextBlocks($user, $exportDestination, $output);
$this->tagsMigrationService->exportTags($user, $exportDestination, $output);
Expand All @@ -65,6 +68,7 @@ public function import(IUser $user, IImportSource $importSource, OutputInterface
$this->deleteExistingData($user, $output);

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

$this->appConfigMigrationService->deleteAppConfiguration($user, $output);
$this->internalAddressesMigrationService->removeInternalAddresses($user, $output);
$this->trustedSendersMigrationService->removeAllTrustedSenders($user, $output);
$this->textBlocksMigrationService->deleteAllTextBlocks($user, $output);
$this->tagsMigrationService->deleteAllTags($user, $output);
Expand Down
127 changes: 127 additions & 0 deletions lib/UserMigration/Service/InternalAddressesMigrationService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Mail\UserMigration\Service;

use JsonException;
use OCA\Mail\Contracts\IInternalAddressService;
use OCA\Mail\UserMigration\MailAccountMigrator;
use OCP\IL10N;
use OCP\IUser;
use OCP\UserMigration\IExportDestination;
use OCP\UserMigration\IImportSource;
use OCP\UserMigration\UserMigrationException;
use Symfony\Component\Console\Output\OutputInterface;

class InternalAddressesMigrationService {
public const INTERNAL_ADDRESSES_FILE = MailAccountMigrator::EXPORT_ROOT . '/internal_addresses.json';

public function __construct(
private readonly IInternalAddressService $internalAddressService,
private readonly IL10N $l10n,
) {
}

/**
* Export all addresses the user defined as internal ones
* on export.
*
* @param IUser $user
* @param IExportDestination $exportDestination
* @param OutputInterface $output
* @throws UserMigrationException
*/
public function exportInternalAddresses(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void {
$output->writeln(
$this->l10n->t('Exporting internal addresses for user %s', [$user->getUID()]),
OutputInterface::VERBOSITY_VERBOSE
);

$internalAddresses = $this->internalAddressService->getInternalAddresses($user->getUID());

try {
$exportDestination->addFileContents(self::INTERNAL_ADDRESSES_FILE, json_encode($internalAddresses, JSON_THROW_ON_ERROR));
} catch (JsonException|UserMigrationException $exception) {
throw new UserMigrationException(
"Failed to export internal addresses for user {$user->getUID()}",
previous: $exception
);
}
}

/**
* Import all addresses the user defined as internal ones.
*
* @throws UserMigrationException
*/
public function importInternalAddresses(IUser $user, IImportSource $importSource, OutputInterface $output): void {
$output->writeln(
$this->l10n->t('Importing internal addresses for user %s', [$user->getUID()]),
OutputInterface::VERBOSITY_VERBOSE
);

$internalAddresses = json_decode($importSource->getFileContents(self::INTERNAL_ADDRESSES_FILE), true);
$this->validateInternalAddresses($internalAddresses);

foreach ($internalAddresses as $internalAddress) {
$this->internalAddressService->add($user->getUID(), $internalAddress['address'], $internalAddress['type']);
}
}

public function removeInternalAddresses(IUser $user, OutputInterface $output): void {
$output->writeln(
$this->l10n->t('Deleting all internal addresses for user %s', [$user->getUID()]),
OutputInterface::VERBOSITY_VERBOSE
);

$this->internalAddressService->removeInternalAddresses($user->getUID());
}

/**
* Validate the parsed internal addresses to ensure they
* have the expected structure and types.
*
* @throws UserMigrationException
*/
private function validateInternalAddresses(mixed $internalAddresses): void {
$internalAddressesArrayIsValid = is_array($internalAddresses) && array_is_list($internalAddresses);
if (!$internalAddressesArrayIsValid) {
throw new UserMigrationException('Invalid internal addresses export structure');
}

foreach ($internalAddresses as $internalAddress) {
$internalAddressArrayIsValid = is_array($internalAddress);

$idIsValid = $internalAddressArrayIsValid
&& array_key_exists('id', $internalAddress)
&& is_int($internalAddress['id']);

$addressIsValid = $internalAddressArrayIsValid
&& array_key_exists('address', $internalAddress)
&& is_string($internalAddress['address']);

$uidIsValid = $internalAddressArrayIsValid
&& array_key_exists('uid', $internalAddress)
&& is_string($internalAddress['uid']);

$typeIsValid = $internalAddressArrayIsValid
&& array_key_exists('type', $internalAddress)
&& is_string($internalAddress['type']);

if (
!$idIsValid
|| !$addressIsValid
|| !$uidIsValid
|| !$typeIsValid
) {
throw new UserMigrationException('Invalid internal address entry');
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace Unit\UserMigration\Service;

use ChristophWurst\Nextcloud\Testing\ServiceMockObject;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Db\InternalAddress;
use OCA\Mail\UserMigration\Service\InternalAddressesMigrationService;
use OCP\IUser;
use OCP\UserMigration\IExportDestination;
use OCP\UserMigration\IImportSource;
use Symfony\Component\Console\Output\OutputInterface;

class InternalAddressesMigrationServiceTest extends TestCase {
private const USER_ID = '123';
private OutputInterface $output;
private IUser $user;
private IExportDestination $exportDestination;
private IImportSource $importSource;
private ServiceMockObject $serviceMock;
private InternalAddressesMigrationService $migrationService;

protected function setUp(): void {
parent::setUp();

$this->serviceMock = $this->createServiceMock(InternalAddressesMigrationService::class);
$this->migrationService = $this->serviceMock->getService();

$this->user = $this->createMock(IUser::class);
$this->user->method('getUID')->willReturn(self::USER_ID);

$this->output = $this->createMock(OutputInterface::class);
$this->exportDestination = $this->createMock(IExportDestination::class);
$this->importSource = $this->createMock(IImportSource::class);
}

public function testExportsMultipleInternalAddresses(): void {
$trustedSendersList = [$this->getTrustedIndividual(), $this->getTrustedDomain()];
$this->exportDestination->expects(self::once())->method('addFileContents')->with(InternalAddressesMigrationService::INTERNAL_ADDRESSES_FILE, json_encode($trustedSendersList));

$this->serviceMock->getParameter('internalAddressService')->method('getInternalAddresses')->with(self::USER_ID)->willReturn($trustedSendersList);

$this->migrationService->exportInternalAddresses($this->user, $this->exportDestination, $this->output);
}

public function testExportsNoneInternalAddress(): void {
$trustedSendersList = [];
$this->exportDestination->expects(self::once())->method('addFileContents')->with(InternalAddressesMigrationService::INTERNAL_ADDRESSES_FILE, json_encode($trustedSendersList));

$this->serviceMock->getParameter('internalAddressService')->method('getInternalAddresses')->with(self::USER_ID)->willReturn($trustedSendersList);

$this->migrationService->exportInternalAddresses($this->user, $this->exportDestination, $this->output);
}

public function testImportMultipleInternalAddresses(): void {
$trustedIndividual = $this->getTrustedIndividual();
$trustedDomain = $this->getTrustedDomain();
$trustedSendersList = [$trustedIndividual, $trustedDomain];
$this->importSource->expects(self::once())->method('getFileContents')->with(InternalAddressesMigrationService::INTERNAL_ADDRESSES_FILE)->willReturn(json_encode($trustedSendersList));

$this->serviceMock->getParameter('internalAddressService')->expects(self::exactly(2))->method('add')->with(self::USER_ID, self::callback(function ($email) use ($trustedIndividual, $trustedDomain) {
return $email === $trustedIndividual->getAddress() || $email === $trustedDomain->getAddress();
}), self::callback(function ($type) use ($trustedIndividual, $trustedDomain) {
return $type === $trustedIndividual->getType() || $type === $trustedDomain->getType();
}));

$this->migrationService->importInternalAddresses($this->user, $this->importSource, $this->output);
}

public function testImportNoneInternalAddress(): void {
$trustedSendersList = [];
$this->importSource->expects(self::once())->method('getFileContents')->with(InternalAddressesMigrationService::INTERNAL_ADDRESSES_FILE)->willReturn(json_encode($trustedSendersList));
$this->serviceMock->getParameter('internalAddressService')->expects(self::never())->method('add');

$this->migrationService->importInternalAddresses($this->user, $this->importSource, $this->output);
}

private function getTrustedIndividual(): InternalAddress {
$individualSender = new InternalAddress;

$individualSender->setId(1);
$individualSender->setUserId(self::USER_ID);
$individualSender->setAddress('max@mustermann.com');
$individualSender->setType('individual');

return $individualSender;
}

private function getTrustedDomain(): InternalAddress {
$domainSender = new InternalAddress();

$domainSender->setId(2);
$domainSender->setUserId(self::USER_ID);
$domainSender->setAddress('nextcloud.com');
$domainSender->setType('domain');

return $domainSender;
}
}
Loading