diff --git a/lib/Contracts/IMailManager.php b/lib/Contracts/IMailManager.php index e498f2721f..e2c7775283 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 array $specialUseAttributes * * @return Mailbox * * @throws ServiceException */ - public function createMailbox(Account $account, string $name): 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 b3485df6d6..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 @@ -246,10 +255,21 @@ public function update() { * @throws ClientException */ #[TrapError] - public function create(int $accountId, string $name): JSONResponse { - $account = $this->accountService->find($this->currentUserId, $accountId); + 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)); + } - return new JSONResponse($this->mailManager->createMailbox($account, $name)); + $account = $this->accountService->find($this->currentUserId, $accountId); + 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 f8892e3cf2..ddbc36e90f 100644 --- a/lib/IMAP/FolderMapper.php +++ b/lib/IMAP/FolderMapper.php @@ -74,10 +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 $specialUseAttributes + * + * @return Folder + * @throws Horde_Imap_Client_Exception + * @throws ServiceException + */ + public function createFolder(Horde_Imap_Client_Socket $client, Account $account, - string $name): Folder { - $client->createMailbox($name); + string $name, + array $specialUseAttributes = []): Folder { + $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 6603c64c32..8052eb9610 100644 --- a/lib/Service/MailManager.php +++ b/lib/Service/MailManager.php @@ -144,19 +144,20 @@ public function getMailboxes(Account $account): array { /** * @param Account $account * @param string $name + * @param array $specialUseAttributes * * @return Mailbox * @throws ServiceException */ #[\Override] - public function createMailbox(Account $account, string $name): Mailbox { + public function createMailbox(Account $account, string $name, array $specialUseAttributes = []): Mailbox { $client = $this->imapClientFactory->getClient($account); try { - $folder = $this->folderMapper->createFolder($client, $account, $name); + $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 a525ff7005..5f3cba0ad1 100644 --- a/src/components/NewMessageModal.vue +++ b/src/components/NewMessageModal.vue @@ -118,7 +118,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' @@ -286,6 +286,57 @@ export default { handleShow(element) { this.additionalTrapElements.push(element) }, + 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) { + 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 + } + + try { + await this.setSentMailboxAndResend(account, newSentMailboxId, data) + 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) { + logger.debug('setting sent mailbox to ' + id) + await this.mainStore.patchAccount({ + account, + data: { + sentMailboxId: id, + }, + }) + }, /** * @param data Message data * @param {object=} opts Options @@ -385,6 +436,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 }) if (this.sending) { @@ -498,7 +554,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..fbe5e0117e 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, specialUseAttributes) { const url = generateUrl('/apps/mail/api/mailboxes') const data = { accountId, name, + 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 86065552d8..eb2b00fd7d 100644 --- a/src/store/mainStore/actions.js +++ b/src/store/mainStore/actions.js @@ -303,12 +303,13 @@ export default function mainStoreActions() { async createMailbox({ account, name, + specialUseAttributes = [], }) { 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, specialUseAttributes) 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..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') + 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') + 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') + 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') + expect(MailboxService.create).toHaveBeenCalledWith(13, 'INBOX.Archive.2020', []) }) it('combines unified inbox even if no inboxes are present', async() => {