44
55/**
66 * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7- * SPDX-License-Identifier: AGPL-3.0-only
7+ * SPDX-License-Identifier: AGPL-3.0-or-later
88 */
99
1010namespace OCA \Mail \UserMigration ;
1111
12- use Exception ;
13- use JsonException ;
14- use OCA \Mail \Db \Alias ;
15- use OCA \Mail \Db \MailAccount ;
16- use OCA \Mail \Service \AccountService ;
17- use OCA \Mail \Service \AliasesService ;
12+ use OCA \Mail \AppInfo \Application ;
13+ use OCA \Mail \Exception \ClientException ;
14+ use OCA \Mail \Exception \ServiceException ;
15+ use OCA \Mail \UserMigration \Service \AccountMigrationService ;
16+ use OCP \AppFramework \Db \DoesNotExistException ;
1817use OCP \IL10N ;
1918use OCP \IUser ;
2019use OCP \Security \ICrypto ;
2322use OCP \UserMigration \IMigrator ;
2423use OCP \UserMigration \UserMigrationException ;
2524use Symfony \Component \Console \Output \OutputInterface ;
26- use function array_map ;
27- use function json_decode ;
28- use function json_encode ;
2925
3026class MailAccountMigrator implements IMigrator {
27+ public const EXPORT_ROOT = Application::APP_ID ;
28+ public const FILENAME_PLACEHOLDER = '{filename} ' ;
3129
3230 public function __construct (
33- private AccountService $ accountService ,
34- private AliasesService $ aliasesService ,
35- private IL10N $ l10n ,
36- private ICrypto $ crypto ,
31+ private readonly IL10N $ l10n ,
32+ private readonly ICrypto $ crypto ,
33+ private readonly AccountMigrationService $ accountMigrationService ,
3734 ) {
3835 }
3936
@@ -42,140 +39,31 @@ public function export(IUser $user,
4239 IExportDestination $ exportDestination ,
4340 OutputInterface $ output ,
4441 ): void {
45- $ accounts = $ this ->accountService ->findByUserId ($ user ->getUID ());
46- $ index = [];
47- foreach ($ accounts as $ account ) {
48- if ($ account ->getMailAccount ()->getProvisioningId () !== null ) {
49- // These configuration of these accounts is owned by the admins
50- $ output ->writeln ("Skipping provisioned account {$ account ->getId ()}" );
51- continue ;
52- }
53-
54- $ accountFilePath = "mail/accounts/ {$ account ->getId ()}.json " ;
55- $ accountData = $ account ->jsonSerialize ();
56-
57- if ($ account ->getMailAccount ()->getAuthMethod () === 'password ' ) {
58- $ encryptedInboundPassword = $ account ->getMailAccount ()->getInboundPassword ();
59- $ encryptedOutboundPassword = $ account ->getMailAccount ()->getOutboundPassword ();
60- if ($ encryptedInboundPassword !== null ) {
61- try {
62- $ accountData ['inboundPassword ' ] = $ this ->crypto ->decrypt ($ encryptedInboundPassword );
63- } catch (Exception $ e ) {
64- $ output ->writeln ("Can not decrypt inbound password of account {$ account ->getId ()}: " . $ e ->getMessage ());
65- }
66- }
67- if ($ encryptedOutboundPassword !== null ) {
68- try {
69- $ accountData ['outboundPassword ' ] = $ this ->crypto ->decrypt ($ encryptedOutboundPassword );
70- } catch (Exception $ e ) {
71- $ output ->writeln ("Can not decrypt outbound password of account {$ account ->getId ()}: " . $ e ->getMessage ());
72- }
73- }
74- } elseif ($ account ->getMailAccount ()->getAuthMethod () === 'xoauth2 ' ) {
75- $ encryptedRefreshToken = $ account ->getMailAccount ()->getOauthRefreshToken ();
76- $ encryptedAccessToken = $ account ->getMailAccount ()->getOauthAccessToken ();
77- if ($ encryptedRefreshToken !== null ) {
78- try {
79- $ accountData ['oauthRefreshToken ' ] = $ this ->crypto ->decrypt ($ encryptedRefreshToken );
80- } catch (Exception $ e ) {
81- $ output ->writeln ("Can not decrypt oauth refresh token of account {$ account ->getId ()}: " . $ e ->getMessage ());
82- }
83- }
84- if ($ encryptedAccessToken !== null ) {
85- try {
86- $ accountData ['oauthAccessToken ' ] = $ this ->crypto ->decrypt ($ encryptedAccessToken );
87- } catch (Exception $ e ) {
88- $ output ->writeln ("Can not decrypt oauth access token of account {$ account ->getId ()}: " . $ e ->getMessage ());
89- }
90- }
91- $ accountData ['oauthTokenTtl ' ] = $ account ->getMailAccount ()->getOauthTokenTtl ();
92- }
93-
94- unset(
95- $ accountData ['draftsMailboxId ' ],
96- $ accountData ['sentMailboxId ' ],
97- $ accountData ['trashMailboxId ' ],
98- $ accountData ['archiveMailboxId ' ],
99- $ accountData ['snoozeMailboxId ' ],
100- $ accountData ['junkMailboxId ' ],
101- );
102-
103- $ aliases = $ this ->aliasesService ->findAll (
104- $ account ->getId (),
105- $ account ->getUserId (), // perf: this adds overhead - add dedicated method to fetch by account id only
106- );
107- $ accountData ['aliases ' ] = array_map (function (Alias $ alias ) {
108- $ data = $ alias ->jsonSerialize ();
109- return $ data ;
110- }, $ aliases );
111-
112- $ exportDestination ->addFileContents ($ accountFilePath , json_encode ($ accountData ));
113- $ index [$ account ->getId ()] = $ accountFilePath ;
114- }
115-
116- $ exportDestination ->addFileContents ('mail/accounts/index.json ' , json_encode ($ index ));
42+ $ output ->writeln ($ this ->l10n ->t ("Exporting mail accounts for user {$ user ->getUID ()}" ), OutputInterface::VERBOSITY_VERBOSE );
11743 }
11844
11945 #[\Override]
12046 public function import (IUser $ user , IImportSource $ importSource , OutputInterface $ output ): void {
121- try {
122- $ index = json_decode ($ importSource ->getFileContents ('mail/accounts/index.json ' ), true , flags: JSON_THROW_ON_ERROR );
123- } catch (JsonException $ e ) {
124- throw new UserMigrationException ("Invalid index content: {$ e ->getMessage ()}" , $ e ->getCode (), $ e );
125- }
126- foreach ($ index as $ accountFilePath ) {
127- try {
128- $ accountData = json_decode ($ importSource ->getFileContents ($ accountFilePath ), true , flags: JSON_THROW_ON_ERROR );
129- } catch (JsonException $ e ) {
130- throw new UserMigrationException ("Invalid account content: {$ e ->getMessage ()}" , $ e ->getCode (), $ e );
131- }
132-
133- // Wipe the old ID(s) to prevent overwrites
134- unset(
135- $ accountData ['id ' ],
136- $ accountData ['accountId ' ],
137- );
138-
139- $ newAccount = new MailAccount ($ accountData );
140-
141- // Change UID to new owner
142- $ newAccount ->setUserId ($ user ->getUID ());
143- // Map the rest of the properties that are not mapped via the constructor
144- $ newAccount ->setName ($ accountData ['name ' ]);
145- $ newAccount ->setAuthMethod ($ accountData ['authMethod ' ]);
146- $ newAccount ->setEditorMode ($ accountData ['editorMode ' ] ?? 'plain ' );
147- $ newAccount ->setSearchBody ($ accountData ['searchBody ' ] ?? false );
148- $ newAccount ->setClassificationEnabled ($ accountData ['classificationEnabled ' ] ?? false );
149- $ newAccount ->setSignatureAboveQuote ($ accountData ['signatureAboveQuote ' ] ?? false );
150- $ newAccount ->setPersonalNamespace ($ accountData ['personalNamespace ' ] ?? null );
151- if (isset ($ accountData ['inboundPassword ' ])) {
152- $ newAccount ->setInboundPassword ($ this ->crypto ->encrypt ($ accountData ['inboundPassword ' ]));
153- }
154- if (isset ($ accountData ['outboundPassword ' ])) {
155- $ newAccount ->setOutboundPassword ($ this ->crypto ->encrypt ($ accountData ['outboundPassword ' ]));
156- }
157- if (isset ($ accountData ['oauthRefreshToken ' ])) {
158- $ newAccount ->setOauthRefreshToken ($ this ->crypto ->encrypt ($ accountData ['oauthRefreshToken ' ]));
159- }
160- if (isset ($ accountData ['oauthAccessToken ' ])) {
161- $ newAccount ->setOauthAccessToken ($ this ->crypto ->encrypt ($ accountData ['oauthAccessToken ' ]));
162- }
163- $ newAccount ->setOauthTokenTtl ($ accountData ['oauthTokenTtl ' ] ?? null );
164-
165- $ mailAccount = $ this ->accountService ->save (
166- $ newAccount
167- );
168-
169- // Import aliases
170- foreach ($ accountData ['aliases ' ] as $ alias ) {
171- $ this ->aliasesService ->create (
172- $ user ->getUID (),
173- $ mailAccount ->getId (),
174- $ alias ['alias ' ],
175- $ alias ['name ' ],
176- );
177- }
178- }
47+ $ output ->writeln ($ this ->l10n ->t ("Importing mail accounts for user {$ user ->getUID ()}" ), OutputInterface::VERBOSITY_VERBOSE );
48+
49+ $ this ->deleteExistingData ($ user , $ output );
50+
51+ $ this ->accountMigrationService ->scheduleBackgroundJobs ($ user , $ output );
52+ }
53+
54+ /**
55+ * Delete all existing user data of our app to ensure
56+ * the result of the import is always the same.
57+ *
58+ * @param IUser $user
59+ * @param OutputInterface $output
60+ * @throws ClientException
61+ * @throws DoesNotExistException
62+ * @throws ServiceException
63+ */
64+ private function deleteExistingData (IUser $ user , OutputInterface $ output ): void {
65+ $ output ->writeln ($ this ->l10n ->t ("Deleting existing mail data for user {$ user ->getUID ()}" ), OutputInterface::VERBOSITY_VERBOSE );
66+ $ this ->accountMigrationService ->deleteAllAccounts ($ user , $ output );
17967 }
18068
18169 #[\Override]
@@ -195,7 +83,7 @@ public function getDescription(): string {
19583
19684 #[\Override]
19785 public function getVersion (): int {
198- return 01_00_00 ;
86+ return 02_00_00 ;
19987 }
20088
20189 #[\Override]
0 commit comments