From 3503febe7d76cca8a8dd273b0672b64056419199 Mon Sep 17 00:00:00 2001 From: hamza221 Date: Wed, 29 Mar 2023 00:59:21 +0200 Subject: [PATCH 1/8] fix: no sent mailbox configured Signed-off-by: hamza221 --- appinfo/routes.php | 5 +++ lib/Contracts/IMailManager.php | 3 +- lib/Controller/MailboxesController.php | 4 +- lib/IMAP/FolderMapper.php | 9 ++++- lib/Service/MailManager.php | 5 ++- src/components/NewMessageModal.vue | 53 +++++++++++++++++++++++++- src/service/MailboxService.js | 3 +- src/store/mainStore/actions.js | 3 +- src/tests/unit/store/actions.spec.js | 8 ++-- 9 files changed, 78 insertions(+), 15 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index eddd1a5ee3..2b79a1e739 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -155,6 +155,11 @@ 'url' => '/api/mailboxes/{id}', 'verb' => 'PATCH' ], + [ + 'name' => 'mailboxes#setImapSpecialUseAtrribute', + 'url' => '/api/mailboxes/{id}/special-use', + 'verb' => 'PUT' + ], [ 'name' => 'mailboxes#sync', 'url' => '/api/mailboxes/{id}/sync', diff --git a/lib/Contracts/IMailManager.php b/lib/Contracts/IMailManager.php index e498f2721f..fc938ead1a 100644 --- a/lib/Contracts/IMailManager.php +++ b/lib/Contracts/IMailManager.php @@ -45,12 +45,13 @@ public function getMailboxes(Account $account): array; /** * @param Account $account * @param string $name + * @param bool $isSentMailbox * * @return Mailbox * * @throws ServiceException */ - public function createMailbox(Account $account, string $name): Mailbox; + public function createMailbox(Account $account, string $name, bool $isSentMailbox = false): Mailbox; /** * @param Mailbox $mailbox diff --git a/lib/Controller/MailboxesController.php b/lib/Controller/MailboxesController.php index b3485df6d6..b3966476d8 100644 --- a/lib/Controller/MailboxesController.php +++ b/lib/Controller/MailboxesController.php @@ -246,10 +246,10 @@ public function update() { * @throws ClientException */ #[TrapError] - public function create(int $accountId, string $name): JSONResponse { + public function create(int $accountId, string $name, bool $isSentMailbox = false): JSONResponse { $account = $this->accountService->find($this->currentUserId, $accountId); - return new JSONResponse($this->mailManager->createMailbox($account, $name)); + return new JSONResponse($this->mailManager->createMailbox($account, $name, $isSentMailbox)); } /** diff --git a/lib/IMAP/FolderMapper.php b/lib/IMAP/FolderMapper.php index f8892e3cf2..3057d48328 100644 --- a/lib/IMAP/FolderMapper.php +++ b/lib/IMAP/FolderMapper.php @@ -76,8 +76,13 @@ public function getFolders(Account $account, Horde_Imap_Client_Socket $client, public function createFolder(Horde_Imap_Client_Socket $client, Account $account, - string $name): Folder { - $client->createMailbox($name); + string $name, + bool $isSentMailbox = false): Folder { + $opts = []; + if ($isSentMailbox) { + $opts[] = Horde_Imap_Client::SPECIALUSE_SENT; + } + $client->createMailbox($name, $opts); $list = $client->listMailboxes($name, Horde_Imap_Client::MBOX_ALL_SUBSCRIBED, [ 'delimiter' => true, diff --git a/lib/Service/MailManager.php b/lib/Service/MailManager.php index 0efc64545d..c22975c50c 100644 --- a/lib/Service/MailManager.php +++ b/lib/Service/MailManager.php @@ -142,14 +142,15 @@ public function getMailboxes(Account $account): array { /** * @param Account $account * @param string $name + * @param bool $isSentMailbox * * @return Mailbox * @throws ServiceException */ - public function createMailbox(Account $account, string $name): Mailbox { + public function createMailbox(Account $account, string $name, bool $isSentMailbox = false): Mailbox { $client = $this->imapClientFactory->getClient($account); try { - $folder = $this->folderMapper->createFolder($client, $account, $name); + $folder = $this->folderMapper->createFolder($client, $account, $name, $isSentMailbox); $this->folderMapper->fetchFolderAcls([$folder], $client); } catch (Horde_Imap_Client_Exception $e) { throw new ServiceException( diff --git a/src/components/NewMessageModal.vue b/src/components/NewMessageModal.vue index 5f1fc6bdd5..96c381d179 100644 --- a/src/components/NewMessageModal.vue +++ b/src/components/NewMessageModal.vue @@ -129,7 +129,7 @@ import { NcEmptyContent as EmptyContent, NcModal as Modal, } from '@nextcloud/vue' -import { showError, showSuccess } from '@nextcloud/dialogs' +import { showError, showSuccess, showWarning } from '@nextcloud/dialogs' import { translate as t } from '@nextcloud/l10n' import logger from '../logger.js' @@ -297,6 +297,51 @@ export default { }, toHtml, plain, + async onNewSentMailbox(data, account) { + showWarning(t('mail', 'Setting Sent default folder')) + let newSentMailboxId = null + const mailboxes = this.mainStore.getMailboxes(data.accountId) + const sentMailboxId = mailboxes.find((mailbox) => mailbox.name === account.personalNamespace + 'Sent' || mailbox.name === account.personalNamespace + t('mail', 'Sent'))?.databaseId + if (sentMailboxId) { + await this.setSentMailboxAndResend(account, sentMailboxId, data) + } + logger.info(`creating ${t('mail', 'Sent')} mailbox`) + try { + const newSentMailbox = await this.mainStore.createMailbox({ account, name: account.personalNamespace + t('mail', 'Sent') }) + logger.info(`mailbox ${account.personalNamespace + t('mail', 'Sent')} created`) + newSentMailboxId = newSentMailbox.databaseId + } catch (error) { + showError(t('mail', 'Could not create new mailbox, please try setting a sent mailbox manually')) + logger.error('could not create mailbox', { error }) + this.$emit('close') + } + + if (newSentMailboxId) { + await this.setSentMailboxAndResend(account, newSentMailboxId, data) + } else { + showError(t('mail', 'Couldn\'t set sent default folder, please try manually before sending a new message')) + this.$emit('close') + } + }, + + async setSentMailboxAndResend(account, id, data) { + logger.debug('setting sent mailbox to ' + id) + try { + await this.mainStore.patchAccount({ + account, + data: { + sentMailboxId: id, + }, + }) + logger.debug('Resending message after new setting sent mailbox') + this.onSend(data) + showSuccess(t('mail', 'Sent folder set, resending message')) + } catch (error) { + showError(t('mail', 'Couldn\'t set sent default folder, please try manually before sending a new message')) + logger.error('could not set sent mailbox', { error }) + this.$emit('close') + } + }, /** * @param data Message data * @param {object=} opts Options @@ -395,6 +440,11 @@ export default { .catch((error) => logger.error('could not upload attachments', { error })) }, async onSend(data, force = false) { + const account = this.mainStore.getAccount(data.accountId) + if (!account?.sentMailboxId) { + this.onNewSentMailbox(data, account) + return + } logger.debug('sending message', { data }) await this.attachmentsPromise @@ -505,7 +555,6 @@ export default { } // Sync sent mailbox when it's currently open - const account = this.mainStore.getAccount(data.accountId) if (account && parseInt(this.$route.params.mailboxId, 10) === account.sentMailboxId) { setTimeout(() => { this.mainStore.syncEnvelopes({ diff --git a/src/service/MailboxService.js b/src/service/MailboxService.js index ff5f42f030..3424b78a5e 100644 --- a/src/service/MailboxService.js +++ b/src/service/MailboxService.js @@ -17,12 +17,13 @@ export async function fetchAll(accountId) { return resp.data.mailboxes } -export function create(accountId, name) { +export function create(accountId, name, isSentMailbox) { const url = generateUrl('/apps/mail/api/mailboxes') const data = { accountId, name, + isSentMailbox, } return axios.post(url, data).then((resp) => resp.data) } diff --git a/src/store/mainStore/actions.js b/src/store/mainStore/actions.js index 02cffdd26d..3595fc1212 100644 --- a/src/store/mainStore/actions.js +++ b/src/store/mainStore/actions.js @@ -278,12 +278,13 @@ export default function mainStoreActions() { async createMailbox({ account, name, + isSentMailbox = false, }) { return handleHttpAuthErrors(async () => { const prefixed = (account.personalNamespace && !name.startsWith(account.personalNamespace)) ? account.personalNamespace + name : name - const mailbox = await createMailbox(account.id, prefixed) + const mailbox = await createMailbox(account.id, prefixed, isSentMailbox) console.debug(`mailbox ${prefixed} created for account ${account.id}`, { mailbox }) this.addMailboxMutation({ account, diff --git a/src/tests/unit/store/actions.spec.js b/src/tests/unit/store/actions.spec.js index 418d498540..66ba933b58 100644 --- a/src/tests/unit/store/actions.spec.js +++ b/src/tests/unit/store/actions.spec.js @@ -63,7 +63,7 @@ describe('Vuex store actions', () => { const result = await store.createMailbox({ account, name }) expect(result).toEqual(mailbox) - expect(MailboxService.create).toHaveBeenCalledWith(13, 'Important') + expect(MailboxService.create).toHaveBeenCalledWith(13, 'Important', false) }) it('creates a sub-mailbox', async () => { @@ -84,7 +84,7 @@ describe('Vuex store actions', () => { const result = await store.createMailbox({ account, name }) expect(result).toEqual(mailbox) - expect(MailboxService.create).toHaveBeenCalledWith(13, 'Archive.2020') + expect(MailboxService.create).toHaveBeenCalledWith(13, 'Archive.2020', false) }) it('adds a prefix to new mailboxes if the account has a personal namespace', async () => { @@ -105,7 +105,7 @@ describe('Vuex store actions', () => { const result = await store.createMailbox({ account, name }) expect(result).toEqual(mailbox) - expect(MailboxService.create).toHaveBeenCalledWith(13, 'INBOX.Important') + expect(MailboxService.create).toHaveBeenCalledWith(13, 'INBOX.Important', false) }) it('adds no prefix to new sub-mailboxes if the account has a personal namespace', async () => { @@ -126,7 +126,7 @@ describe('Vuex store actions', () => { const result = await store.createMailbox({ account, name }) expect(result).toEqual(mailbox) - expect(MailboxService.create).toHaveBeenCalledWith(13, 'INBOX.Archive.2020') + expect(MailboxService.create).toHaveBeenCalledWith(13, 'INBOX.Archive.2020', false) }) it('combines unified inbox even if no inboxes are present', async() => { From 3f66fea106a0603661e0f91178272dfa4dc974a1 Mon Sep 17 00:00:00 2001 From: Hamza Mahjoubi Date: Mon, 17 Mar 2025 12:54:14 +0100 Subject: [PATCH 2/8] fixup! fix: no sent mailbox configured Signed-off-by: Hamza Mahjoubi --- appinfo/routes.php | 5 ----- lib/Contracts/IMailManager.php | 4 ++-- lib/Controller/MailboxesController.php | 4 ++-- lib/IMAP/FolderMapper.php | 20 +++++++++++++++++--- lib/Service/MailManager.php | 8 ++++---- src/components/NewMessageModal.vue | 2 +- src/service/MailboxService.js | 4 ++-- src/store/mainStore/actions.js | 4 ++-- tests/Unit/IMAP/FolderMapperTest.php | 11 +++++++++++ 9 files changed, 41 insertions(+), 21 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index 2b79a1e739..eddd1a5ee3 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -155,11 +155,6 @@ 'url' => '/api/mailboxes/{id}', 'verb' => 'PATCH' ], - [ - 'name' => 'mailboxes#setImapSpecialUseAtrribute', - 'url' => '/api/mailboxes/{id}/special-use', - 'verb' => 'PUT' - ], [ 'name' => 'mailboxes#sync', 'url' => '/api/mailboxes/{id}/sync', diff --git a/lib/Contracts/IMailManager.php b/lib/Contracts/IMailManager.php index fc938ead1a..dbaccd7a5c 100644 --- a/lib/Contracts/IMailManager.php +++ b/lib/Contracts/IMailManager.php @@ -45,13 +45,13 @@ public function getMailboxes(Account $account): array; /** * @param Account $account * @param string $name - * @param bool $isSentMailbox + * @param array $specialUseAttributes * * @return Mailbox * * @throws ServiceException */ - public function createMailbox(Account $account, string $name, bool $isSentMailbox = false): Mailbox; + public function createMailbox(Account $account, string $name, array $specialUseAttributes = []): Mailbox; /** * @param Mailbox $mailbox diff --git a/lib/Controller/MailboxesController.php b/lib/Controller/MailboxesController.php index b3966476d8..8fb9dfe5e1 100644 --- a/lib/Controller/MailboxesController.php +++ b/lib/Controller/MailboxesController.php @@ -246,10 +246,10 @@ public function update() { * @throws ClientException */ #[TrapError] - public function create(int $accountId, string $name, bool $isSentMailbox = false): JSONResponse { + public function create(int $accountId, string $name, array $specialUseAttributes = []): JSONResponse { $account = $this->accountService->find($this->currentUserId, $accountId); - return new JSONResponse($this->mailManager->createMailbox($account, $name, $isSentMailbox)); + return new JSONResponse($this->mailManager->createMailbox($account, $name, $specialUseAttributes)); } /** diff --git a/lib/IMAP/FolderMapper.php b/lib/IMAP/FolderMapper.php index 3057d48328..3dde718dc9 100644 --- a/lib/IMAP/FolderMapper.php +++ b/lib/IMAP/FolderMapper.php @@ -39,6 +39,16 @@ public function __construct(LoggerInterface $logger) { 'INBOX.dovecot.sieve' ]; + private const SUPPORTED_SPECIAL_USE_ATTRIBUTES = [ + Horde_Imap_Client::SPECIALUSE_ALL, + Horde_Imap_Client::SPECIALUSE_ARCHIVE, + Horde_Imap_Client::SPECIALUSE_DRAFTS, + Horde_Imap_Client::SPECIALUSE_FLAGGED, + Horde_Imap_Client::SPECIALUSE_JUNK, + Horde_Imap_Client::SPECIALUSE_SENT, + Horde_Imap_Client::SPECIALUSE_TRASH, + ]; + /** * @param Account $account * @param Horde_Imap_Client_Socket $client @@ -77,10 +87,14 @@ public function getFolders(Account $account, Horde_Imap_Client_Socket $client, public function createFolder(Horde_Imap_Client_Socket $client, Account $account, string $name, - bool $isSentMailbox = false): Folder { + array $specialUseAttributes = []): Folder { $opts = []; - if ($isSentMailbox) { - $opts[] = Horde_Imap_Client::SPECIALUSE_SENT; + + foreach ($specialUseAttributes as $attribute) { + if (!in_array($attribute, self::SUPPORTED_SPECIAL_USE_ATTRIBUTES, true)) { + throw new ServiceException('Unsupported special use attribute: ' . $attribute); + } + $opts[] = $attribute; } $client->createMailbox($name, $opts); diff --git a/lib/Service/MailManager.php b/lib/Service/MailManager.php index c22975c50c..7c4f923898 100644 --- a/lib/Service/MailManager.php +++ b/lib/Service/MailManager.php @@ -142,19 +142,19 @@ public function getMailboxes(Account $account): array { /** * @param Account $account * @param string $name - * @param bool $isSentMailbox + * @param array $specialUseAttributes * * @return Mailbox * @throws ServiceException */ - public function createMailbox(Account $account, string $name, bool $isSentMailbox = false): Mailbox { + public function createMailbox(Account $account, string $name, array $specialUseAttributes = []): Mailbox { $client = $this->imapClientFactory->getClient($account); try { - $folder = $this->folderMapper->createFolder($client, $account, $name, $isSentMailbox); + $folder = $this->folderMapper->createFolder($client, $account, $name, $specialUseAttributes); $this->folderMapper->fetchFolderAcls([$folder], $client); } catch (Horde_Imap_Client_Exception $e) { throw new ServiceException( - 'Could not get mailbox status: ' . $e->getMessage(), + 'Could not create Mailbox: ' . $e->getMessage(), $e->getCode(), $e ); diff --git a/src/components/NewMessageModal.vue b/src/components/NewMessageModal.vue index 96c381d179..3ef36394ed 100644 --- a/src/components/NewMessageModal.vue +++ b/src/components/NewMessageModal.vue @@ -307,7 +307,7 @@ export default { } logger.info(`creating ${t('mail', 'Sent')} mailbox`) try { - const newSentMailbox = await this.mainStore.createMailbox({ account, name: account.personalNamespace + t('mail', 'Sent') }) + const newSentMailbox = await this.mainStore.createMailbox({ account, name: account.personalNamespace + t('mail', 'Sent'), specialUseAttributes: ['\\Sent'] }) logger.info(`mailbox ${account.personalNamespace + t('mail', 'Sent')} created`) newSentMailboxId = newSentMailbox.databaseId } catch (error) { diff --git a/src/service/MailboxService.js b/src/service/MailboxService.js index 3424b78a5e..fbe5e0117e 100644 --- a/src/service/MailboxService.js +++ b/src/service/MailboxService.js @@ -17,13 +17,13 @@ export async function fetchAll(accountId) { return resp.data.mailboxes } -export function create(accountId, name, isSentMailbox) { +export function create(accountId, name, specialUseAttributes) { const url = generateUrl('/apps/mail/api/mailboxes') const data = { accountId, name, - isSentMailbox, + specialUseAttributes, } return axios.post(url, data).then((resp) => resp.data) } diff --git a/src/store/mainStore/actions.js b/src/store/mainStore/actions.js index 3595fc1212..e946598382 100644 --- a/src/store/mainStore/actions.js +++ b/src/store/mainStore/actions.js @@ -278,13 +278,13 @@ export default function mainStoreActions() { async createMailbox({ account, name, - isSentMailbox = false, + specialUseAttributes = [], }) { return handleHttpAuthErrors(async () => { const prefixed = (account.personalNamespace && !name.startsWith(account.personalNamespace)) ? account.personalNamespace + name : name - const mailbox = await createMailbox(account.id, prefixed, isSentMailbox) + const mailbox = await createMailbox(account.id, prefixed, specialUseAttributes) console.debug(`mailbox ${prefixed} created for account ${account.id}`, { mailbox }) this.addMailboxMutation({ account, diff --git a/tests/Unit/IMAP/FolderMapperTest.php b/tests/Unit/IMAP/FolderMapperTest.php index 38644c2d9b..83db0d0b77 100644 --- a/tests/Unit/IMAP/FolderMapperTest.php +++ b/tests/Unit/IMAP/FolderMapperTest.php @@ -157,6 +157,17 @@ public function testCreateFolder(): void { $this->assertEquals($expected, $created); } + public function testCreateFolderUnsupportedSpecialUse(): void { + $account = $this->createMock(Account::class); + $client = $this->createMock(Horde_Imap_Client_Socket::class); + $client->expects($this->never()) + ->method('createMailbox'); + $client->expects($this->never()) + ->method('listMailboxes'); + $this->expectExceptionMessage('Unsupported special use attribute: \\unsupported'); + $this->mapper->createFolder($client, $account, 'new', ['\\unsupported']); + } + public function testFetchFoldersAclsNoSelect(): void { $folders = [ $this->createMock(Folder::class), From c19736e1682b49c06fd3cb5ad312d960aa62b667 Mon Sep 17 00:00:00 2001 From: Hamza Mahjoubi Date: Mon, 17 Mar 2025 16:38:00 +0100 Subject: [PATCH 3/8] fixup! fix: no sent mailbox configured Signed-off-by: Hamza Mahjoubi --- lib/Controller/MailboxesController.php | 8 ++++++-- lib/IMAP/FolderMapper.php | 9 +++++---- src/tests/unit/store/actions.spec.js | 8 ++++---- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/Controller/MailboxesController.php b/lib/Controller/MailboxesController.php index 8fb9dfe5e1..9f6151a95f 100644 --- a/lib/Controller/MailboxesController.php +++ b/lib/Controller/MailboxesController.php @@ -248,8 +248,12 @@ public function update() { #[TrapError] public function create(int $accountId, string $name, array $specialUseAttributes = []): JSONResponse { $account = $this->accountService->find($this->currentUserId, $accountId); - - return new JSONResponse($this->mailManager->createMailbox($account, $name, $specialUseAttributes)); + try { + $mailbox = $this->mailManager->createMailbox($account, $name, $specialUseAttributes); + } catch (ServiceException $e) { + return new JSONResponse([], Http::STATUS_BAD_REQUEST); + } + return new JSONResponse($mailbox); } /** diff --git a/lib/IMAP/FolderMapper.php b/lib/IMAP/FolderMapper.php index 3dde718dc9..cddb60b5de 100644 --- a/lib/IMAP/FolderMapper.php +++ b/lib/IMAP/FolderMapper.php @@ -88,15 +88,16 @@ public function createFolder(Horde_Imap_Client_Socket $client, Account $account, string $name, array $specialUseAttributes = []): Folder { - $opts = []; + $attributes = []; foreach ($specialUseAttributes as $attribute) { if (!in_array($attribute, self::SUPPORTED_SPECIAL_USE_ATTRIBUTES, true)) { - throw new ServiceException('Unsupported special use attribute: ' . $attribute); + $this->logger->error("Unsupported special use attribute: $attribute"); + throw new ServiceException("Unsupported special use attribute: $attribute"); } - $opts[] = $attribute; + $attributes[] = $attribute; } - $client->createMailbox($name, $opts); + $client->createMailbox($name, ['special_use' => $attributes]); $list = $client->listMailboxes($name, Horde_Imap_Client::MBOX_ALL_SUBSCRIBED, [ 'delimiter' => true, diff --git a/src/tests/unit/store/actions.spec.js b/src/tests/unit/store/actions.spec.js index 66ba933b58..4a468715b2 100644 --- a/src/tests/unit/store/actions.spec.js +++ b/src/tests/unit/store/actions.spec.js @@ -63,7 +63,7 @@ describe('Vuex store actions', () => { const result = await store.createMailbox({ account, name }) expect(result).toEqual(mailbox) - expect(MailboxService.create).toHaveBeenCalledWith(13, 'Important', false) + expect(MailboxService.create).toHaveBeenCalledWith(13, 'Important', []) }) it('creates a sub-mailbox', async () => { @@ -84,7 +84,7 @@ describe('Vuex store actions', () => { const result = await store.createMailbox({ account, name }) expect(result).toEqual(mailbox) - expect(MailboxService.create).toHaveBeenCalledWith(13, 'Archive.2020', false) + expect(MailboxService.create).toHaveBeenCalledWith(13, 'Archive.2020', []) }) it('adds a prefix to new mailboxes if the account has a personal namespace', async () => { @@ -105,7 +105,7 @@ describe('Vuex store actions', () => { const result = await store.createMailbox({ account, name }) expect(result).toEqual(mailbox) - expect(MailboxService.create).toHaveBeenCalledWith(13, 'INBOX.Important', false) + expect(MailboxService.create).toHaveBeenCalledWith(13, 'INBOX.Important', []) }) it('adds no prefix to new sub-mailboxes if the account has a personal namespace', async () => { @@ -126,7 +126,7 @@ describe('Vuex store actions', () => { const result = await store.createMailbox({ account, name }) expect(result).toEqual(mailbox) - expect(MailboxService.create).toHaveBeenCalledWith(13, 'INBOX.Archive.2020', false) + expect(MailboxService.create).toHaveBeenCalledWith(13, 'INBOX.Archive.2020', []) }) it('combines unified inbox even if no inboxes are present', async() => { From 79f9ef3783c42fda4222644bdb797e1682832dac Mon Sep 17 00:00:00 2001 From: Hamza Mahjoubi Date: Mon, 17 Mar 2025 19:13:25 +0100 Subject: [PATCH 4/8] fixup! fix: no sent mailbox configured Signed-off-by: Hamza Mahjoubi --- lib/Contracts/IMailManager.php | 2 +- lib/Controller/MailboxesController.php | 18 ++++++++++++++- lib/IMAP/FolderMapper.php | 32 ++++++++++---------------- lib/Service/MailManager.php | 2 +- tests/Unit/IMAP/FolderMapperTest.php | 11 --------- 5 files changed, 31 insertions(+), 34 deletions(-) diff --git a/lib/Contracts/IMailManager.php b/lib/Contracts/IMailManager.php index dbaccd7a5c..ffc3d405ee 100644 --- a/lib/Contracts/IMailManager.php +++ b/lib/Contracts/IMailManager.php @@ -45,7 +45,7 @@ public function getMailboxes(Account $account): array; /** * @param Account $account * @param string $name - * @param array $specialUseAttributes + * @param array<"\\All","\\Archive",'\\Drafts',"\\Flagged","\\Junk","\\Sent","\\Trash"> $specialUseAttributes * * @return Mailbox * diff --git a/lib/Controller/MailboxesController.php b/lib/Controller/MailboxesController.php index 9f6151a95f..768f29e112 100644 --- a/lib/Controller/MailboxesController.php +++ b/lib/Controller/MailboxesController.php @@ -35,7 +35,16 @@ class MailboxesController extends Controller { private ?string $currentUserId; private IMailManager $mailManager; private SyncService $syncService; - + + private const SUPPORTED_SPECIAL_USE_ATTRIBUTES = [ + Horde_Imap_Client::SPECIALUSE_ALL, + Horde_Imap_Client::SPECIALUSE_ARCHIVE, + Horde_Imap_Client::SPECIALUSE_DRAFTS, + Horde_Imap_Client::SPECIALUSE_FLAGGED, + Horde_Imap_Client::SPECIALUSE_JUNK, + Horde_Imap_Client::SPECIALUSE_SENT, + Horde_Imap_Client::SPECIALUSE_TRASH, + ]; /** * @param string $appName * @param IRequest $request @@ -247,6 +256,13 @@ public function update() { */ #[TrapError] public function create(int $accountId, string $name, array $specialUseAttributes = []): JSONResponse { + $diff = array_filter($specialUseAttributes, static function ($attribute) { + return !in_array($attribute, self::SUPPORTED_SPECIAL_USE_ATTRIBUTES, true); + }); + if (!empty($diff)) { + throw new ServiceException('Unsupported special use attribute: ' . implode(', ', $diff)); + } + $account = $this->accountService->find($this->currentUserId, $accountId); try { $mailbox = $this->mailManager->createMailbox($account, $name, $specialUseAttributes); diff --git a/lib/IMAP/FolderMapper.php b/lib/IMAP/FolderMapper.php index cddb60b5de..09d517f3bf 100644 --- a/lib/IMAP/FolderMapper.php +++ b/lib/IMAP/FolderMapper.php @@ -39,16 +39,6 @@ public function __construct(LoggerInterface $logger) { 'INBOX.dovecot.sieve' ]; - private const SUPPORTED_SPECIAL_USE_ATTRIBUTES = [ - Horde_Imap_Client::SPECIALUSE_ALL, - Horde_Imap_Client::SPECIALUSE_ARCHIVE, - Horde_Imap_Client::SPECIALUSE_DRAFTS, - Horde_Imap_Client::SPECIALUSE_FLAGGED, - Horde_Imap_Client::SPECIALUSE_JUNK, - Horde_Imap_Client::SPECIALUSE_SENT, - Horde_Imap_Client::SPECIALUSE_TRASH, - ]; - /** * @param Account $account * @param Horde_Imap_Client_Socket $client @@ -84,20 +74,22 @@ public function getFolders(Account $account, Horde_Imap_Client_Socket $client, }, $toPersist); } + /** + * @param Horde_Imap_Client_Socket $client + * @param Account $account + * @param string $name + * @param array<"\\All","\\Archive",'\\Drafts',"\\Flagged","\\Junk","\\Sent","\\Trash"> $specialUseAttributes + * + * @return Folder + * @throws Horde_Imap_Client_Exception + * @throws ServiceException + */ + public function createFolder(Horde_Imap_Client_Socket $client, Account $account, string $name, array $specialUseAttributes = []): Folder { - $attributes = []; - - foreach ($specialUseAttributes as $attribute) { - if (!in_array($attribute, self::SUPPORTED_SPECIAL_USE_ATTRIBUTES, true)) { - $this->logger->error("Unsupported special use attribute: $attribute"); - throw new ServiceException("Unsupported special use attribute: $attribute"); - } - $attributes[] = $attribute; - } - $client->createMailbox($name, ['special_use' => $attributes]); + $client->createMailbox($name, ['special_use' => $specialUseAttributes]); $list = $client->listMailboxes($name, Horde_Imap_Client::MBOX_ALL_SUBSCRIBED, [ 'delimiter' => true, diff --git a/lib/Service/MailManager.php b/lib/Service/MailManager.php index 7c4f923898..0854b894d8 100644 --- a/lib/Service/MailManager.php +++ b/lib/Service/MailManager.php @@ -142,7 +142,7 @@ public function getMailboxes(Account $account): array { /** * @param Account $account * @param string $name - * @param array $specialUseAttributes + * @param array<"\\All","\\Archive",'\\Drafts',"\\Flagged","\\Junk","\\Sent","\\Trash"> $specialUseAttributes * * @return Mailbox * @throws ServiceException diff --git a/tests/Unit/IMAP/FolderMapperTest.php b/tests/Unit/IMAP/FolderMapperTest.php index 83db0d0b77..38644c2d9b 100644 --- a/tests/Unit/IMAP/FolderMapperTest.php +++ b/tests/Unit/IMAP/FolderMapperTest.php @@ -157,17 +157,6 @@ public function testCreateFolder(): void { $this->assertEquals($expected, $created); } - public function testCreateFolderUnsupportedSpecialUse(): void { - $account = $this->createMock(Account::class); - $client = $this->createMock(Horde_Imap_Client_Socket::class); - $client->expects($this->never()) - ->method('createMailbox'); - $client->expects($this->never()) - ->method('listMailboxes'); - $this->expectExceptionMessage('Unsupported special use attribute: \\unsupported'); - $this->mapper->createFolder($client, $account, 'new', ['\\unsupported']); - } - public function testFetchFoldersAclsNoSelect(): void { $folders = [ $this->createMock(Folder::class), From ee751f7b8b5f23f124cf3625973bec7868b1da32 Mon Sep 17 00:00:00 2001 From: Hamza Mahjoubi Date: Mon, 17 Mar 2025 19:17:39 +0100 Subject: [PATCH 5/8] fixup! fix: no sent mailbox configured Signed-off-by: Hamza Mahjoubi --- lib/Contracts/IMailManager.php | 2 +- lib/IMAP/FolderMapper.php | 2 +- lib/Service/MailManager.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Contracts/IMailManager.php b/lib/Contracts/IMailManager.php index ffc3d405ee..ca9c88778a 100644 --- a/lib/Contracts/IMailManager.php +++ b/lib/Contracts/IMailManager.php @@ -45,7 +45,7 @@ public function getMailboxes(Account $account): array; /** * @param Account $account * @param string $name - * @param array<"\\All","\\Archive",'\\Drafts',"\\Flagged","\\Junk","\\Sent","\\Trash"> $specialUseAttributes + * @param array<"\\All"|"\\Archive"|"\\Drafts"|"\\Flagged"|"\\Junk"|"\\Sent"|"\\Trash"> $specialUseAttributes * * @return Mailbox * diff --git a/lib/IMAP/FolderMapper.php b/lib/IMAP/FolderMapper.php index 09d517f3bf..2254726872 100644 --- a/lib/IMAP/FolderMapper.php +++ b/lib/IMAP/FolderMapper.php @@ -78,7 +78,7 @@ public function getFolders(Account $account, Horde_Imap_Client_Socket $client, * @param Horde_Imap_Client_Socket $client * @param Account $account * @param string $name - * @param array<"\\All","\\Archive",'\\Drafts',"\\Flagged","\\Junk","\\Sent","\\Trash"> $specialUseAttributes + * @param array<"\\All"|"\\Archive"|"\\Drafts"|"\\Flagged"|"\\Junk"|"\\Sent"|"\\Trash"> $specialUseAttributes * * @return Folder * @throws Horde_Imap_Client_Exception diff --git a/lib/Service/MailManager.php b/lib/Service/MailManager.php index 0854b894d8..fb3b753689 100644 --- a/lib/Service/MailManager.php +++ b/lib/Service/MailManager.php @@ -142,7 +142,7 @@ public function getMailboxes(Account $account): array { /** * @param Account $account * @param string $name - * @param array<"\\All","\\Archive",'\\Drafts',"\\Flagged","\\Junk","\\Sent","\\Trash"> $specialUseAttributes + * @param array<"\\All"|"\\Archive"|"\\Drafts"|"\\Flagged"|"\\Junk"|"\\Sent"|"\\Trash"> $specialUseAttributes * * @return Mailbox * @throws ServiceException From d56d4538bbacd58e13b6fbd62ab6f893e0db0ae3 Mon Sep 17 00:00:00 2001 From: Hamza Mahjoubi Date: Thu, 3 Apr 2025 12:44:57 +0200 Subject: [PATCH 6/8] fixup! fix: no sent mailbox configured Signed-off-by: Hamza Mahjoubi --- lib/Contracts/IMailManager.php | 3 ++- lib/IMAP/FolderMapper.php | 2 +- lib/Service/MailManager.php | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/Contracts/IMailManager.php b/lib/Contracts/IMailManager.php index ca9c88778a..1ceb5c0c0b 100644 --- a/lib/Contracts/IMailManager.php +++ b/lib/Contracts/IMailManager.php @@ -10,6 +10,7 @@ namespace OCA\Mail\Contracts; use Horde_Imap_Client_Socket; +use Horde_Imap_Client; use OCA\Mail\Account; use OCA\Mail\Attachment; use OCA\Mail\Db\Mailbox; @@ -45,7 +46,7 @@ public function getMailboxes(Account $account): array; /** * @param Account $account * @param string $name - * @param array<"\\All"|"\\Archive"|"\\Drafts"|"\\Flagged"|"\\Junk"|"\\Sent"|"\\Trash"> $specialUseAttributes + * @param array $specialUseAttributes * * @return Mailbox * diff --git a/lib/IMAP/FolderMapper.php b/lib/IMAP/FolderMapper.php index 2254726872..ddbc36e90f 100644 --- a/lib/IMAP/FolderMapper.php +++ b/lib/IMAP/FolderMapper.php @@ -78,7 +78,7 @@ public function getFolders(Account $account, Horde_Imap_Client_Socket $client, * @param Horde_Imap_Client_Socket $client * @param Account $account * @param string $name - * @param array<"\\All"|"\\Archive"|"\\Drafts"|"\\Flagged"|"\\Junk"|"\\Sent"|"\\Trash"> $specialUseAttributes + * @param array $specialUseAttributes * * @return Folder * @throws Horde_Imap_Client_Exception diff --git a/lib/Service/MailManager.php b/lib/Service/MailManager.php index fb3b753689..11ba29e0d8 100644 --- a/lib/Service/MailManager.php +++ b/lib/Service/MailManager.php @@ -142,7 +142,7 @@ public function getMailboxes(Account $account): array { /** * @param Account $account * @param string $name - * @param array<"\\All"|"\\Archive"|"\\Drafts"|"\\Flagged"|"\\Junk"|"\\Sent"|"\\Trash"> $specialUseAttributes + * @param array $specialUseAttributes * * @return Mailbox * @throws ServiceException From a9cbe36ea1b89133bd2fac9d8af934277495127a Mon Sep 17 00:00:00 2001 From: Hamza Date: Sat, 24 May 2025 18:24:35 +0200 Subject: [PATCH 7/8] fixup! fix: no sent mailbox configured Signed-off-by: Hamza --- lib/Contracts/IMailManager.php | 1 - src/components/NewMessageModal.vue | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/Contracts/IMailManager.php b/lib/Contracts/IMailManager.php index 1ceb5c0c0b..e2c7775283 100644 --- a/lib/Contracts/IMailManager.php +++ b/lib/Contracts/IMailManager.php @@ -10,7 +10,6 @@ namespace OCA\Mail\Contracts; use Horde_Imap_Client_Socket; -use Horde_Imap_Client; use OCA\Mail\Account; use OCA\Mail\Attachment; use OCA\Mail\Db\Mailbox; diff --git a/src/components/NewMessageModal.vue b/src/components/NewMessageModal.vue index d48b6e98fe..7f4b23b75e 100644 --- a/src/components/NewMessageModal.vue +++ b/src/components/NewMessageModal.vue @@ -290,14 +290,14 @@ export default { showWarning(t('mail', 'Setting Sent default folder')) let newSentMailboxId = null const mailboxes = this.mainStore.getMailboxes(data.accountId) - const sentMailboxId = mailboxes.find((mailbox) => mailbox.name === account.personalNamespace + 'Sent' || mailbox.name === account.personalNamespace + t('mail', 'Sent'))?.databaseId + const sentMailboxId = mailboxes.find((mailbox) => (mailbox.name === (account.personalNamespace ?? '') + 'Sent') || (mailbox.name === (account.personalNamespace ?? '') + t('mail', 'Sent')))?.databaseId if (sentMailboxId) { await this.setSentMailboxAndResend(account, sentMailboxId, data) } logger.info(`creating ${t('mail', 'Sent')} mailbox`) try { - const newSentMailbox = await this.mainStore.createMailbox({ account, name: account.personalNamespace + t('mail', 'Sent'), specialUseAttributes: ['\\Sent'] }) - logger.info(`mailbox ${account.personalNamespace + t('mail', 'Sent')} created`) + const newSentMailbox = await this.mainStore.createMailbox({ account, name: (account.personalNamespace ?? '') + t('mail', 'Sent'), specialUseAttributes: ['\\Sent'] }) + logger.info(`mailbox ${(account.personalNamespace ?? '') + t('mail', 'Sent')} created`) newSentMailboxId = newSentMailbox.databaseId } catch (error) { showError(t('mail', 'Could not create new mailbox, please try setting a sent mailbox manually')) From 5a3ab6199d0767174282d6160b74fbc0946e8bff Mon Sep 17 00:00:00 2001 From: Hamza Date: Thu, 5 Jun 2025 13:12:55 +0200 Subject: [PATCH 8/8] fixup! fix: no sent mailbox configured Signed-off-by: Hamza --- src/components/NewMessageModal.vue | 46 +++++++++++++++++------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/src/components/NewMessageModal.vue b/src/components/NewMessageModal.vue index e7be92e424..5f3cba0ad1 100644 --- a/src/components/NewMessageModal.vue +++ b/src/components/NewMessageModal.vue @@ -287,49 +287,55 @@ export default { this.additionalTrapElements.push(element) }, async onNewSentMailbox(data, account) { - showWarning(t('mail', 'Setting Sent default folder')) + showWarning(t('mail', 'Setting Sent default folder...')) let newSentMailboxId = null const mailboxes = this.mainStore.getMailboxes(data.accountId) const sentMailboxId = mailboxes.find((mailbox) => (mailbox.name === (account.personalNamespace ?? '') + 'Sent') || (mailbox.name === (account.personalNamespace ?? '') + t('mail', 'Sent')))?.databaseId if (sentMailboxId) { - await this.setSentMailboxAndResend(account, sentMailboxId, data) + try { + await this.setSentMailboxAndResend(account, sentMailboxId, data) + showSuccess(t('mail', 'Default sent folder set')) + this.onSend(data) + + } catch (error) { + logger.error('could not set sent mailbox', { error }) + showError(t('mail', 'Couldn\'t set sent default folder, please try manually before sending a new message')) + } + return + } logger.info(`creating ${t('mail', 'Sent')} mailbox`) try { const newSentMailbox = await this.mainStore.createMailbox({ account, name: (account.personalNamespace ?? '') + t('mail', 'Sent'), specialUseAttributes: ['\\Sent'] }) + showSuccess(t('mail', 'Default sent folder set')) logger.info(`mailbox ${(account.personalNamespace ?? '') + t('mail', 'Sent')} created`) newSentMailboxId = newSentMailbox.databaseId } catch (error) { showError(t('mail', 'Could not create new mailbox, please try setting a sent mailbox manually')) logger.error('could not create mailbox', { error }) this.$emit('close') + return } - if (newSentMailboxId) { + try { await this.setSentMailboxAndResend(account, newSentMailboxId, data) - } else { + this.onSend(data) + } catch (error) { + logger.error('could not set sent mailbox', { error }) showError(t('mail', 'Couldn\'t set sent default folder, please try manually before sending a new message')) this.$emit('close') } + }, - async setSentMailboxAndResend(account, id, data) { + async setSentMailboxAndResend(account, id) { logger.debug('setting sent mailbox to ' + id) - try { - await this.mainStore.patchAccount({ - account, - data: { - sentMailboxId: id, - }, - }) - logger.debug('Resending message after new setting sent mailbox') - this.onSend(data) - showSuccess(t('mail', 'Sent folder set, resending message')) - } catch (error) { - showError(t('mail', 'Couldn\'t set sent default folder, please try manually before sending a new message')) - logger.error('could not set sent mailbox', { error }) - this.$emit('close') - } + await this.mainStore.patchAccount({ + account, + data: { + sentMailboxId: id, + }, + }) }, /** * @param data Message data