Skip to content

Commit d84f4af

Browse files
committed
feat(UserMigration): Overwork migration to include all settings (quick actions)
Signed-off-by: David Dreschner <david.dreschner@nextcloud.com>
1 parent 6ba1c53 commit d84f4af

3 files changed

Lines changed: 476 additions & 100 deletions

File tree

lib/UserMigration/MailAccountMigrator.php

Lines changed: 105 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use OCA\Mail\UserMigration\Service\AccountMigrationService;
1616
use OCA\Mail\UserMigration\Service\AppConfigMigrationService;
1717
use OCA\Mail\UserMigration\Service\InternalAddressesMigrationService;
18+
use OCA\Mail\UserMigration\Service\QuickActionsMigrationService;
1819
use OCA\Mail\UserMigration\Service\SMIMEMigrationService;
1920
use OCA\Mail\UserMigration\Service\TagsMigrationService;
2021
use OCA\Mail\UserMigration\Service\TextBlocksMigrationService;
@@ -30,105 +31,109 @@
3031
use Symfony\Component\Console\Output\OutputInterface;
3132

3233
class MailAccountMigrator implements IMigrator {
33-
public const EXPORT_ROOT = Application::APP_ID;
34-
public const FILENAME_PLACEHOLDER = '{filename}';
35-
36-
public function __construct(
37-
private readonly IL10N $l10n,
38-
private readonly ICrypto $crypto,
39-
private readonly AccountMigrationService $accountMigrationService,
40-
private readonly AppConfigMigrationService $appConfigMigrationService,
41-
private readonly InternalAddressesMigrationService $internalAddressesMigrationService,
42-
private readonly TrustedSendersMigrationService $trustedSendersMigrationService,
43-
private readonly TextBlocksMigrationService $textBlocksMigrationService,
44-
private readonly TagsMigrationService $tagsMigrationService,
45-
private readonly SMIMEMigrationService $sMimeMigrationService,
46-
) {
47-
}
48-
49-
#[\Override]
50-
public function export(IUser $user,
51-
IExportDestination $exportDestination,
52-
OutputInterface $output,
53-
): void {
54-
$output->writeln($this->l10n->t("Exporting mail accounts for user {$user->getUID()}"), OutputInterface::VERBOSITY_VERBOSE);
55-
56-
$this->appConfigMigrationService->exportAppConfiguration($user, $exportDestination, $output);
57-
$this->internalAddressesMigrationService->exportInternalAddresses($user, $exportDestination, $output);
58-
$this->trustedSendersMigrationService->exportTrustedSenders($user, $exportDestination, $output);
59-
$this->textBlocksMigrationService->exportTextBlocks($user, $exportDestination, $output);
60-
$this->tagsMigrationService->exportTags($user, $exportDestination, $output);
61-
$this->sMimeMigrationService->exportCertificates($user, $exportDestination, $output);
62-
$this->accountMigrationService->exportAccounts($user, $exportDestination, $output);
63-
}
64-
65-
#[\Override]
66-
public function import(IUser $user, IImportSource $importSource, OutputInterface $output): void {
67-
$output->writeln($this->l10n->t("Importing mail accounts for user {$user->getUID()}"), OutputInterface::VERBOSITY_VERBOSE);
68-
69-
$this->deleteExistingData($user, $output);
70-
71-
$this->appConfigMigrationService->importAppConfiguration($user, $importSource, $output);
72-
$this->internalAddressesMigrationService->importInternalAddresses($user, $importSource, $output);
73-
$this->trustedSendersMigrationService->importTrustedSenders($user, $importSource, $output);
74-
$this->textBlocksMigrationService->importTextBlocks($user, $importSource, $output);
75-
$newCertificateIds = $this->sMimeMigrationService->importCertificates($user, $importSource, $output);
76-
$newAccountIds = $this->accountMigrationService->importAccounts($user, $importSource, $output, $newCertificateIds);
77-
$newTagIds = $this->tagsMigrationService->importTags($user, $importSource, $output);
78-
79-
$this->accountMigrationService->scheduleBackgroundJobs($user, $output);
80-
}
81-
82-
/**
83-
* Delete all existing user data of our app to ensure
84-
* the result of the import is always the same.
85-
*
86-
* @param IUser $user
87-
* @param OutputInterface $output
88-
* @throws ClientException
89-
* @throws DoesNotExistException
90-
* @throws ServiceException
91-
*/
92-
private function deleteExistingData(IUser $user, OutputInterface $output): void {
93-
$output->writeln($this->l10n->t("Deleting existing mail data for user {$user->getUID()}"), OutputInterface::VERBOSITY_VERBOSE);
94-
95-
$this->accountMigrationService->deleteAllAccounts($user, $output);
96-
$this->appConfigMigrationService->deleteAppConfiguration($user, $output);
97-
$this->internalAddressesMigrationService->removeInternalAddresses($user, $output);
98-
$this->trustedSendersMigrationService->removeAllTrustedSenders($user, $output);
99-
$this->textBlocksMigrationService->deleteAllTextBlocks($user, $output);
100-
$this->tagsMigrationService->deleteAllTags($user, $output);
101-
$this->accountMigrationService->deleteAllAccounts($user, $output);
102-
$this->sMimeMigrationService->deleteAllUserCertificates($user, $output);
103-
}
104-
105-
#[\Override]
106-
public function getId(): string {
107-
return 'mail_account';
108-
}
109-
110-
#[\Override]
111-
public function getDisplayName(): string {
112-
return $this->l10n->t('Mail');
113-
}
114-
115-
#[\Override]
116-
public function getDescription(): string {
117-
return $this->l10n->t('Mail account parameters, aliases and preferences');
118-
}
119-
120-
#[\Override]
121-
public function getVersion(): int {
122-
return 02_00_00;
123-
}
124-
125-
#[\Override]
126-
public function canImport(IImportSource $importSource): bool {
127-
try {
128-
return $importSource->getMigratorVersion($this->getId()) <= $this->getVersion();
129-
} catch (UserMigrationException) {
130-
return false;
131-
}
132-
}
34+
public const EXPORT_ROOT = Application::APP_ID;
35+
public const FILENAME_PLACEHOLDER = '{filename}';
36+
37+
public function __construct(
38+
private readonly IL10N $l10n,
39+
private readonly ICrypto $crypto,
40+
private readonly AccountMigrationService $accountMigrationService,
41+
private readonly AppConfigMigrationService $appConfigMigrationService,
42+
private readonly InternalAddressesMigrationService $internalAddressesMigrationService,
43+
private readonly TrustedSendersMigrationService $trustedSendersMigrationService,
44+
private readonly TextBlocksMigrationService $textBlocksMigrationService,
45+
private readonly TagsMigrationService $tagsMigrationService,
46+
private readonly SMIMEMigrationService $sMimeMigrationService,
47+
private readonly QuickActionsMigrationService $quickActionsMigrationService,
48+
) {
49+
}
50+
51+
#[\Override]
52+
public function export(IUser $user,
53+
IExportDestination $exportDestination,
54+
OutputInterface $output,
55+
): void {
56+
$output->writeln($this->l10n->t("Exporting mail accounts for user {$user->getUID()}"), OutputInterface::VERBOSITY_VERBOSE);
57+
58+
$this->appConfigMigrationService->exportAppConfiguration($user, $exportDestination, $output);
59+
$this->internalAddressesMigrationService->exportInternalAddresses($user, $exportDestination, $output);
60+
$this->trustedSendersMigrationService->exportTrustedSenders($user, $exportDestination, $output);
61+
$this->textBlocksMigrationService->exportTextBlocks($user, $exportDestination, $output);
62+
$this->tagsMigrationService->exportTags($user, $exportDestination, $output);
63+
$this->sMimeMigrationService->exportCertificates($user, $exportDestination, $output);
64+
$this->accountMigrationService->exportAccounts($user, $exportDestination, $output);
65+
$this->quickActionsMigrationService->exportQuickActions($user, $exportDestination, $output);
66+
}
67+
68+
#[\Override]
69+
public function import(IUser $user, IImportSource $importSource, OutputInterface $output): void {
70+
$output->writeln($this->l10n->t("Importing mail accounts for user {$user->getUID()}"), OutputInterface::VERBOSITY_VERBOSE);
71+
72+
$this->deleteExistingData($user, $output);
73+
74+
$this->appConfigMigrationService->importAppConfiguration($user, $importSource, $output);
75+
$this->internalAddressesMigrationService->importInternalAddresses($user, $importSource, $output);
76+
$this->trustedSendersMigrationService->importTrustedSenders($user, $importSource, $output);
77+
$this->textBlocksMigrationService->importTextBlocks($user, $importSource, $output);
78+
$newCertificateIds = $this->sMimeMigrationService->importCertificates($user, $importSource, $output);
79+
$newAccountAndMailboxIds = $this->accountMigrationService->importAccounts($user, $importSource, $output, $newCertificateIds);
80+
$newTagIds = $this->tagsMigrationService->importTags($user, $importSource, $output);
81+
$this->quickActionsMigrationService->importQuickActions($user, $importSource, $output, $newAccountAndMailboxIds, $newTagIds);
82+
83+
$this->accountMigrationService->scheduleBackgroundJobs($user, $output);
84+
}
85+
86+
/**
87+
* Delete all existing user data of our app to ensure
88+
* the result of the import is always the same.
89+
*
90+
* @param IUser $user
91+
* @param OutputInterface $output
92+
* @throws ClientException
93+
* @throws DoesNotExistException
94+
* @throws ServiceException
95+
*/
96+
private function deleteExistingData(IUser $user, OutputInterface $output): void {
97+
$output->writeln($this->l10n->t("Deleting existing mail data for user {$user->getUID()}"), OutputInterface::VERBOSITY_VERBOSE);
98+
99+
$this->quickActionsMigrationService->deleteAllQuickActions($user, $output);
100+
$this->accountMigrationService->deleteAllAccounts($user, $output);
101+
$this->appConfigMigrationService->deleteAppConfiguration($user, $output);
102+
$this->internalAddressesMigrationService->removeInternalAddresses($user, $output);
103+
$this->trustedSendersMigrationService->removeAllTrustedSenders($user, $output);
104+
$this->textBlocksMigrationService->deleteAllTextBlocks($user, $output);
105+
$this->tagsMigrationService->deleteAllTags($user, $output);
106+
$this->accountMigrationService->deleteAllAccounts($user, $output);
107+
$this->sMimeMigrationService->deleteAllUserCertificates($user, $output);
108+
}
109+
110+
#[\Override]
111+
public function getId(): string {
112+
return 'mail_account';
113+
}
114+
115+
#[\Override]
116+
public function getDisplayName(): string {
117+
return $this->l10n->t('Mail');
118+
}
119+
120+
#[\Override]
121+
public function getDescription(): string {
122+
return $this->l10n->t('Mail account parameters, aliases and preferences');
123+
}
124+
125+
#[\Override]
126+
public function getVersion(): int {
127+
return 02_00_00;
128+
}
129+
130+
#[\Override]
131+
public function canImport(IImportSource $importSource): bool {
132+
try {
133+
return $importSource->getMigratorVersion($this->getId()) <= $this->getVersion();
134+
} catch (UserMigrationException) {
135+
return false;
136+
}
137+
}
133138

134139
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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\Service\QuickActionsService;
14+
use OCA\Mail\UserMigration\MailAccountMigrator;
15+
use OCP\DB\Exception;
16+
use OCP\IL10N;
17+
use OCP\IUser;
18+
use OCP\UserMigration\IExportDestination;
19+
use OCP\UserMigration\IImportSource;
20+
use OCP\UserMigration\UserMigrationException;
21+
use Symfony\Component\Console\Output\OutputInterface;
22+
23+
class QuickActionsMigrationService {
24+
public const QUICK_ACTIONS_FILE = MailAccountMigrator::EXPORT_ROOT . '/quick_actions.json';
25+
26+
public function __construct(
27+
private readonly QuickActionsService $quickActionsService,
28+
private readonly IL10N $l10n,
29+
) {
30+
}
31+
32+
/**
33+
* Export all quick actions the user defined across
34+
* their accounts.
35+
*
36+
* @param IUser $user
37+
* @param IExportDestination $exportDestination
38+
* @param OutputInterface $output
39+
* @throws UserMigrationException
40+
*/
41+
public function exportQuickActions(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void {
42+
$output->writeln(
43+
$this->l10n->t('Exporting quick actions for user %s', [$user->getUID()]),
44+
OutputInterface::VERBOSITY_VERBOSE
45+
);
46+
47+
$quickActions = $this->quickActionsService->findAll($user->getUID());
48+
49+
try {
50+
$exportDestination->addFileContents(self::QUICK_ACTIONS_FILE, json_encode($quickActions, JSON_THROW_ON_ERROR));
51+
} catch (JsonException|UserMigrationException $exception) {
52+
throw new UserMigrationException(
53+
"Failed to export quick actions for user {$user->getUID()}",
54+
previous: $exception
55+
);
56+
}
57+
}
58+
59+
/**
60+
* Import all quick actions the user defined across
61+
* their accounts.
62+
*
63+
* @throws UserMigrationException
64+
* @throws Exception
65+
* @throws JsonException
66+
* @throws \OCA\Mail\Exception\ServiceException
67+
*/
68+
public function importQuickActions(IUser $user, IImportSource $importSource, OutputInterface $output, array $accountAndMailboxMapping, array $tagMapping): void {
69+
$output->writeln(
70+
$this->l10n->t('Importing quick actions for user %s', [$user->getUID()]),
71+
OutputInterface::VERBOSITY_VERBOSE
72+
);
73+
74+
$quickActions = json_decode($importSource->getFileContents(self::QUICK_ACTIONS_FILE), true);
75+
$this->validateQuickActions($quickActions);
76+
77+
foreach ($quickActions as $quickAction) {
78+
$createdQuickAction = $this->quickActionsService->create($quickAction['name'], $accountAndMailboxMapping['accounts'][$quickAction['accountId']]);
79+
80+
foreach ($quickAction['actionSteps'] as $actionStep) {
81+
$this->quickActionsService->createActionStep($actionStep['name'], $actionStep['order'], $createdQuickAction->getId(), $tagMapping[$actionStep['tagId']] ?? null, $accountAndMailboxMapping['mailboxes'][$actionStep['mailboxId']] ?? null);
82+
}
83+
}
84+
}
85+
86+
public function deleteAllQuickActions(IUser $user, OutputInterface $output): void {
87+
$output->writeln(
88+
$this->l10n->t('Deleting all quick actions for user %s', [$user->getUID()]),
89+
OutputInterface::VERBOSITY_VERBOSE
90+
);
91+
92+
$this->quickActionsService->deleteAll($user->getUID());
93+
}
94+
95+
/**
96+
* Validate the parsed quick actions to ensure they
97+
* have the expected structure and types.
98+
*
99+
* @throws UserMigrationException
100+
*/
101+
private function validateQuickActions(mixed $quickActions): void {
102+
$quickActionsArrayIsValid = is_array($quickActions) && array_is_list($quickActions);
103+
if (!$quickActionsArrayIsValid) {
104+
throw new UserMigrationException('Invalid quick actions export structure');
105+
}
106+
107+
foreach ($quickActions as $quickAction) {
108+
$quickActionArrayIsValid = is_array($quickAction);
109+
110+
$idIsValid = $quickActionArrayIsValid
111+
&& array_key_exists('id', $quickAction)
112+
&& is_int($quickAction['id']);
113+
114+
$nameIsValid = $quickActionArrayIsValid
115+
&& array_key_exists('name', $quickAction)
116+
&& is_string($quickAction['name']);
117+
118+
$orderIsValid = $quickActionArrayIsValid
119+
&& array_key_exists('accountId', $quickAction)
120+
&& is_int($quickAction['accountId']);
121+
122+
$actionStepsArrayIsValid = $quickActionArrayIsValid
123+
&& array_key_exists('actionSteps', $quickAction)
124+
&& is_array($quickAction['actionSteps'])
125+
&& array_is_list($quickAction['actionSteps'])
126+
&& $this->validateQuickSteps($quickAction['actionSteps']);
127+
128+
if (
129+
!$idIsValid
130+
|| !$nameIsValid
131+
|| !$orderIsValid
132+
|| !$actionStepsArrayIsValid
133+
) {
134+
throw new UserMigrationException('Invalid quick action entry');
135+
}
136+
}
137+
}
138+
139+
private function validateQuickSteps(mixed $quickSteps): bool {
140+
$quickStepsArrayIsValid = true;
141+
142+
foreach ($quickSteps as $actionStep) {
143+
$actionStepArrayIsValid = is_array($actionStep);
144+
145+
$idIsValid = $actionStepArrayIsValid
146+
&& array_key_exists('id', $actionStep)
147+
&& is_int($actionStep['id']);
148+
149+
$nameIsValid = $actionStepArrayIsValid
150+
&& array_key_exists('name', $actionStep)
151+
&& is_string($actionStep['name']);
152+
153+
$orderIsValid = $actionStepArrayIsValid
154+
&& array_key_exists('order', $actionStep)
155+
&& is_int($actionStep['order']);
156+
157+
$actionIdIsValid = $actionStepArrayIsValid
158+
&& array_key_exists('actionId', $actionStep)
159+
&& is_int($actionStep['actionId']);
160+
161+
$tagIdIsValid = $actionStepArrayIsValid
162+
&& array_key_exists('tagId', $actionStep)
163+
&& (is_int($actionStep['tagId']) || is_null($actionStep['tagId']));
164+
165+
$mailboxIdIsValid = $actionStepArrayIsValid
166+
&& array_key_exists('mailboxId', $actionStep)
167+
&& (is_int($actionStep['mailboxId']) || is_null($actionStep['mailboxId']));
168+
169+
$actionStepIsValid = $idIsValid
170+
&& $nameIsValid
171+
&& $orderIsValid
172+
&& $actionIdIsValid
173+
&& $tagIdIsValid
174+
&& $mailboxIdIsValid;
175+
176+
$quickStepsArrayIsValid = $quickStepsArrayIsValid && $actionStepIsValid;
177+
}
178+
179+
return $quickStepsArrayIsValid;
180+
}
181+
}

0 commit comments

Comments
 (0)