Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/mail.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristoph Wurst <christoph@winzerhof-wurst.at>2022-11-03 12:28:13 +0300
committergreta <gretadoci@gmail.com>2022-11-03 18:28:06 +0300
commitc31336e08a3abe7dd10e691bdbef4102b05d4b54 (patch)
tree3b532e584426f9c77a8946dbd1b6166635be3837
parenteecb86e284e14a3232f2d1bb861bbdb437126312 (diff)
Catch expired session and reload the page
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at> Signed-off-by: greta <gretadoci@gmail.com>
-rw-r--r--src/App.vue21
-rw-r--r--src/http/sessionExpiryHandler.js37
-rw-r--r--src/store/actions.js1595
-rw-r--r--src/store/getters.js3
-rw-r--r--src/store/index.js1
-rw-r--r--src/store/mutations.js3
-rw-r--r--src/tests/unit/App.spec.js66
-rw-r--r--src/tests/unit/http/sessionExpiryHandler.spec.js82
-rw-r--r--src/tests/unit/store/getters.spec.js6
9 files changed, 1079 insertions, 735 deletions
diff --git a/src/App.vue b/src/App.vue
index 10ca26f4b..eeaa12d0d 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -26,6 +26,10 @@
</template>
<script>
+import { mapGetters } from 'vuex'
+import { showError } from '@nextcloud/dialogs'
+import { translate as t } from '@nextcloud/l10n'
+
import logger from './logger'
import { matchError } from './errors/match'
import MailboxLockedError from './errors/MailboxLockedError'
@@ -33,10 +37,24 @@ import MailboxLockedError from './errors/MailboxLockedError'
export default {
name: 'App',
computed: {
+ ...mapGetters([
+ 'isExpiredSession',
+ ]),
hasMailAccounts() {
return !!this.$store.getters.accounts.find((account) => !account.isUnified)
},
},
+ watch: {
+ isExpiredSession(expired) {
+ if (expired) {
+ showError(t('mail', 'Your session expired. The page will be reloaded.'), {
+ onRemove: () => {
+ this.reload()
+ },
+ })
+ }
+ },
+ },
async mounted() {
// Redirect to setup page if no accounts are configured
if (!this.hasMailAccounts) {
@@ -50,6 +68,9 @@ export default {
await this.$store.dispatch('loadCollections')
},
methods: {
+ reload() {
+ window.location.reload()
+ },
sync() {
setTimeout(async () => {
try {
diff --git a/src/http/sessionExpiryHandler.js b/src/http/sessionExpiryHandler.js
new file mode 100644
index 000000000..cafe2ea1d
--- /dev/null
+++ b/src/http/sessionExpiryHandler.js
@@ -0,0 +1,37 @@
+/*
+ * @copyright 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import logger from '../logger'
+
+export async function handleHttpAuthErrors(commit, cb) {
+ try {
+ const res = await cb()
+ logger.debug('req done')
+ return res
+ } catch (error) {
+ logger.debug('req err', { error, status: error.response?.status, message: error.response?.data?.message })
+ if (error.response?.status === 401 && error.response?.data?.message === 'Current user is not logged in') {
+ logger.warn('Request failed due to expired session')
+ commit('setSessionExpired')
+ }
+ throw error
+ }
+}
diff --git a/src/store/actions.js b/src/store/actions.js
index 3be457399..5fc12ecb5 100644
--- a/src/store/actions.js
+++ b/src/store/actions.js
@@ -99,6 +99,7 @@ import {
import { html, plain, toPlain } from '../util/text'
import Axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
+import { handleHttpAuthErrors } from '../http/sessionExpiryHandler'
import { showWarning } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import {
@@ -129,936 +130,1056 @@ const combineEnvelopeLists = pipe(flatten, orderBy(prop('dateInt'), 'desc'))
export default {
savePreference({ commit, getters }, { key, value }) {
- return savePreference(key, value).then(({ value }) => {
+ return handleHttpAuthErrors(commit, async () => {
+ const newValue = await savePreference(key, value)
commit('savePreference', {
key,
- value,
+ value: newValue,
})
})
},
async fetchAccounts({ commit, getters }) {
- const accounts = await fetchAllAccounts()
- accounts.forEach((account) => commit('addAccount', account))
- return getters.accounts
+ return handleHttpAuthErrors(commit, async () => {
+ const accounts = await fetchAllAccounts()
+ accounts.forEach((account) => commit('addAccount', account))
+ return getters.accounts
+ })
},
async fetchAccount({ commit }, id) {
- const account = await fetchAccount(id)
- commit('addAccount', account)
- return account
+ return handleHttpAuthErrors(commit, async () => {
+ const account = await fetchAccount(id)
+ commit('addAccount', account)
+ return account
+ })
},
async createAccount({ commit }, config) {
- const account = await createAccount(config)
- logger.debug(`account ${account.id} created, fetching mailboxes …`, account)
- account.mailboxes = await fetchAllMailboxes(account.id)
- commit('addAccount', account)
- logger.info("new account's mailboxes fetched", { account, config })
- return account
+ return handleHttpAuthErrors(commit, async () => {
+ const account = await createAccount(config)
+ logger.debug(`account ${account.id} created, fetching mailboxes …`, account)
+ account.mailboxes = await fetchAllMailboxes(account.id)
+ commit('addAccount', account)
+ logger.info("new account's mailboxes fetched", { account, config })
+ return account
+ })
},
async updateAccount({ commit }, config) {
- const account = await updateAccount(config)
- logger.debug('account updated', { account })
- commit('editAccount', account)
- return account
+ return handleHttpAuthErrors(commit, async () => {
+ const account = await updateAccount(config)
+ logger.debug('account updated', { account })
+ commit('editAccount', account)
+ return account
+ })
},
async patchAccount({ commit }, { account, data }) {
- const patchedAccount = await patchAccount(account, data)
- logger.debug('account patched', { account: patchedAccount, data })
- commit('patchAccount', { account, data })
- return account
+ return handleHttpAuthErrors(commit, async () => {
+ const patchedAccount = await patchAccount(account, data)
+ logger.debug('account patched', { account: patchedAccount, data })
+ commit('patchAccount', { account, data })
+ return account
+ })
},
async updateAccountSignature({ commit }, { account, signature }) {
- await updateSignature(account, signature)
- logger.debug('account signature updated', { account, signature })
- const updated = Object.assign({}, account, { signature })
- commit('editAccount', updated)
- return account
+ return handleHttpAuthErrors(commit, async () => {
+ await updateSignature(account, signature)
+ logger.debug('account signature updated', { account, signature })
+ const updated = Object.assign({}, account, { signature })
+ commit('editAccount', updated)
+ return account
+ })
},
async setAccountSetting({ commit, getters }, { accountId, key, value }) {
- commit('setAccountSetting', { accountId, key, value })
- return await savePreference('account-settings', JSON.stringify(getters.getAllAccountSettings))
+ return handleHttpAuthErrors(commit, async () => {
+ commit('setAccountSetting', { accountId, key, value })
+ return await savePreference('account-settings', JSON.stringify(getters.getAllAccountSettings))
+ })
},
async deleteAccount({ commit }, account) {
- try {
- await deleteAccount(account.id)
- } catch (error) {
- logger.error('could not delete account', { error })
- throw error
- }
+ return handleHttpAuthErrors(commit, async () => {
+ try {
+ await deleteAccount(account.id)
+ } catch (error) {
+ logger.error('could not delete account', { error })
+ throw error
+ }
+ })
},
async deleteMailbox({ commit }, { mailbox }) {
- await deleteMailbox(mailbox.databaseId)
- commit('removeMailbox', { id: mailbox.databaseId })
+ return handleHttpAuthErrors(commit, async () => {
+ await deleteMailbox(mailbox.databaseId)
+ commit('removeMailbox', { id: mailbox.databaseId })
+ })
},
async clearMailbox({ commit }, { mailbox }) {
- await clearMailbox(mailbox.databaseId)
- commit('removeEnvelopes', { id: mailbox.databaseId })
- commit('setMailboxUnreadCount', { id: mailbox.databaseId })
+ return handleHttpAuthErrors(commit, async () => {
+ await clearMailbox(mailbox.databaseId)
+ commit('removeEnvelopes', { id: mailbox.databaseId })
+ commit('setMailboxUnreadCount', { id: mailbox.databaseId })
+ })
},
async createMailbox({ commit }, { account, name }) {
- const prefixed = (account.personalNamespace && !name.startsWith(account.personalNamespace))
- ? account.personalNamespace + name
- : name
- const mailbox = await createMailbox(account.id, prefixed)
- console.debug(`mailbox ${prefixed} created for account ${account.id}`, { mailbox })
- commit('addMailbox', { account, mailbox })
- commit('expandAccount', account.id)
- commit('setAccountSetting', { accountId: account.id, key: 'collapsed', value: false })
- return mailbox
+ return handleHttpAuthErrors(commit, async () => {
+ const prefixed = (account.personalNamespace && !name.startsWith(account.personalNamespace))
+ ? account.personalNamespace + name
+ : name
+ const mailbox = await createMailbox(account.id, prefixed)
+ console.debug(`mailbox ${prefixed} created for account ${account.id}`, { mailbox })
+ commit('addMailbox', { account, mailbox })
+ commit('expandAccount', account.id)
+ commit('setAccountSetting', {
+ accountId: account.id,
+ key: 'collapsed',
+ value: false,
+ })
+ return mailbox
+ })
},
async moveAccount({ commit, getters }, { account, up }) {
- const accounts = getters.accounts
- const index = accounts.indexOf(account)
- if (up) {
- const previous = accounts[index - 1]
- accounts[index - 1] = account
- accounts[index] = previous
- } else {
- const next = accounts[index + 1]
- accounts[index + 1] = account
- accounts[index] = next
- }
- return await Promise.all(
- accounts.map((account, idx) => {
- if (account.id === 0) {
- return Promise.resolve()
- }
- commit('saveAccountsOrder', { account, order: idx })
- return patchAccount(account, { order: idx })
- })
- )
+ return handleHttpAuthErrors(commit, async () => {
+ const accounts = getters.accounts
+ const index = accounts.indexOf(account)
+ if (up) {
+ const previous = accounts[index - 1]
+ accounts[index - 1] = account
+ accounts[index] = previous
+ } else {
+ const next = accounts[index + 1]
+ accounts[index + 1] = account
+ accounts[index] = next
+ }
+ return await Promise.all(
+ accounts.map((account, idx) => {
+ if (account.id === 0) {
+ return Promise.resolve()
+ }
+ commit('saveAccountsOrder', { account, order: idx })
+ return patchAccount(account, { order: idx })
+ })
+ )
+ })
},
- async markMailboxRead({ getters, dispatch }, { accountId, mailboxId }) {
- const mailbox = getters.getMailbox(mailboxId)
-
- if (mailbox.isUnified) {
- const findIndividual = findIndividualMailboxes(getters.getMailboxes, mailbox.specialRole)
- const individualMailboxes = findIndividual(getters.accounts)
- return Promise.all(
- individualMailboxes.map((mb) =>
- dispatch('markMailboxRead', {
- accountId: mb.accountId,
- mailboxId: mb.databaseId,
- })
+ async markMailboxRead({ commit, getters, dispatch }, { accountId, mailboxId }) {
+ return handleHttpAuthErrors(commit, async () => {
+ const mailbox = getters.getMailbox(mailboxId)
+
+ if (mailbox.isUnified) {
+ const findIndividual = findIndividualMailboxes(getters.getMailboxes, mailbox.specialRole)
+ const individualMailboxes = findIndividual(getters.accounts)
+ return Promise.all(
+ individualMailboxes.map((mb) =>
+ dispatch('markMailboxRead', {
+ accountId: mb.accountId,
+ mailboxId: mb.databaseId,
+ })
+ )
)
- )
- }
+ }
- await markMailboxRead(mailboxId)
- dispatch('syncEnvelopes', {
- accountId,
- mailboxId,
+ await markMailboxRead(mailboxId)
+ dispatch('syncEnvelopes', {
+ accountId,
+ mailboxId,
+ })
})
},
async changeMailboxSubscription({ commit }, { mailbox, subscribed }) {
- logger.debug(`toggle subscription for mailbox ${mailbox.databaseId}`, {
- mailbox,
- subscribed,
- })
- const updated = await patchMailbox(mailbox.databaseId, { subscribed })
+ return handleHttpAuthErrors(commit, async () => {
+ logger.debug(`toggle subscription for mailbox ${mailbox.databaseId}`, {
+ mailbox,
+ subscribed,
+ })
+ const updated = await patchMailbox(mailbox.databaseId, { subscribed })
- commit('updateMailbox', {
- mailbox: updated,
- })
- logger.debug(`subscription for mailbox ${mailbox.databaseId} updated`, {
- mailbox,
- updated,
+ commit('updateMailbox', {
+ mailbox: updated,
+ })
+ logger.debug(`subscription for mailbox ${mailbox.databaseId} updated`, {
+ mailbox,
+ updated,
+ })
})
},
async patchMailbox({ commit }, { mailbox, attributes }) {
- logger.debug('patching mailbox', {
- mailbox,
- attributes,
- })
+ return handleHttpAuthErrors(commit, async () => {
+ logger.debug('patching mailbox', {
+ mailbox,
+ attributes,
+ })
- const updated = await patchMailbox(mailbox.databaseId, attributes)
+ const updated = await patchMailbox(mailbox.databaseId, attributes)
- commit('updateMailbox', {
- mailbox: updated,
- })
- logger.debug(`mailbox ${mailbox.databaseId} patched`, {
- mailbox,
- updated,
+ commit('updateMailbox', {
+ mailbox: updated,
+ })
+ logger.debug(`mailbox ${mailbox.databaseId} patched`, {
+ mailbox,
+ updated,
+ })
})
},
async showMessageComposer({ commit, dispatch, getters }, { type = 'imap', data = {}, reply, forwardedMessages = [], templateMessageId }) {
- if (reply) {
- const original = await dispatch('fetchMessage', reply.data.databaseId)
-
- // Fetch and transform the body into a rich text object
- if (original.hasHtmlBody) {
- const resp = await Axios.get(
- generateUrl('/apps/mail/api/messages/{id}/html?plain=true', {
- id: original.databaseId,
+ return handleHttpAuthErrors(commit, async () => {
+ if (reply) {
+ const original = await dispatch('fetchMessage', reply.data.databaseId)
+
+ // Fetch and transform the body into a rich text object
+ if (original.hasHtmlBody) {
+ const resp = await Axios.get(
+ generateUrl('/apps/mail/api/messages/{id}/html?plain=true', {
+ id: original.databaseId,
+ })
+ )
+
+ resp.data = DOMPurify.sanitize(resp.data, {
+ FORBID_TAGS: ['style'],
})
- )
- resp.data = DOMPurify.sanitize(resp.data, {
- FORBID_TAGS: ['style'],
- })
+ data.body = html(resp.data)
+ } else {
+ data.body = plain(original.body)
+ }
- data.body = html(resp.data)
- } else {
- data.body = plain(original.body)
- }
+ if (reply.mode === 'reply') {
+ logger.debug('Show simple reply composer', { reply })
+ commit('showMessageComposer', {
+ data: {
+ accountId: reply.data.accountId,
+ to: reply.data.from,
+ cc: [],
+ subject: buildReplySubject(reply.data.subject),
+ body: data.body,
+ originalBody: data.body,
+ replyTo: reply.data,
+ },
+ })
+ return
+ } else if (reply.mode === 'replyAll') {
+ logger.debug('Show reply all reply composer', { reply })
+ const account = getters.getAccount(reply.data.accountId)
+ const recipients = buildReplyRecipients(reply.data, {
+ email: account.emailAddress,
+ label: account.name,
+ })
+ commit('showMessageComposer', {
+ data: {
+ accountId: reply.data.accountId,
+ to: recipients.to,
+ cc: recipients.cc,
+ subject: buildReplySubject(reply.data.subject),
+ body: data.body,
+ originalBody: data.body,
+ replyTo: reply.data,
+ },
+ })
+ return
+ } else if (reply.mode === 'forward') {
+ logger.debug('Show forward composer', { reply })
+ commit('showMessageComposer', {
+ data: {
+ accountId: reply.data.accountId,
+ to: [],
+ cc: [],
+ subject: buildForwardSubject(reply.data.subject),
+ body: data.body,
+ originalBody: data.body,
+ forwardFrom: reply.data,
+ attachments: original.attachments.map(attachment => ({
+ ...attachment,
+ mailboxId: original.mailboxId,
+ // messageId for attachments is actually the uid
+ uid: attachment.messageId,
+ type: 'message-attachment',
+ })),
+ },
+ })
+ return
+ }
+ } else if (templateMessageId) {
+ const message = await dispatch('fetchMessage', templateMessageId)
+ // Merge the original into any existing data
+ data = {
+ ...data,
+ message,
+ }
- if (reply.mode === 'reply') {
- logger.debug('Show simple reply composer', { reply })
- commit('showMessageComposer', {
- data: {
- accountId: reply.data.accountId,
- to: reply.data.from,
- cc: [],
- subject: buildReplySubject(reply.data.subject),
- body: data.body,
- originalBody: data.body,
- replyTo: reply.data,
- },
- })
- return
- } else if (reply.mode === 'replyAll') {
- logger.debug('Show reply all reply composer', { reply })
- const account = getters.getAccount(reply.data.accountId)
- const recipients = buildReplyRecipients(reply.data, {
- email: account.emailAddress,
- label: account.name,
- })
- commit('showMessageComposer', {
- data: {
- accountId: reply.data.accountId,
- to: recipients.to,
- cc: recipients.cc,
- subject: buildReplySubject(reply.data.subject),
- body: data.body,
- originalBody: data.body,
- replyTo: reply.data,
- },
- })
- return
- } else if (reply.mode === 'forward') {
- logger.debug('Show forward composer', { reply })
- commit('showMessageComposer', {
- data: {
- accountId: reply.data.accountId,
- to: [],
- cc: [],
- subject: buildForwardSubject(reply.data.subject),
- body: data.body,
- originalBody: data.body,
- forwardFrom: reply.data,
- attachments: original.attachments.map(attachment => ({
- ...attachment,
- mailboxId: original.mailboxId,
- // messageId for attachments is actually the uid
- uid: attachment.messageId,
- type: 'message-attachment',
- })),
- },
- })
- return
- }
- } else if (templateMessageId) {
- const message = await dispatch('fetchMessage', templateMessageId)
- // Merge the original into any existing data
- data = {
- ...data,
- message,
- }
+ // Fetch and transform the body into a rich text object
+ if (message.hasHtmlBody) {
+ const resp = await Axios.get(
+ generateUrl('/apps/mail/api/messages/{id}/html?plain=true', {
+ id: templateMessageId,
+ })
+ )
- // Fetch and transform the body into a rich text object
- if (message.hasHtmlBody) {
- const resp = await Axios.get(
- generateUrl('/apps/mail/api/messages/{id}/html?plain=true', {
- id: templateMessageId,
+ resp.data = DOMPurify.sanitize(resp.data, {
+ FORBID_TAGS: ['style'],
})
- )
- resp.data = DOMPurify.sanitize(resp.data, {
- FORBID_TAGS: ['style'],
- })
+ data.body = html(resp.data)
+ } else {
+ data.body = plain(message.body)
+ }
- data.body = html(resp.data)
- } else {
- data.body = plain(message.body)
+ // TODO: implement attachments
+ if (message.attachments.length) {
+ showWarning(t('mail', 'Attachments were not copied. Please add them manually.'))
+ }
}
- // TODO: implement attachments
- if (message.attachments.length) {
- showWarning(t('mail', 'Attachments were not copied. Please add them manually.'))
- }
- }
-
- // Stop schedule when editing outbox messages and backup sendAt timestamp
- let originalSendAt
- if (type === 'outbox' && data.id && data.sendAt) {
- originalSendAt = data.sendAt
- const message = {
- ...data,
- body: data.isHtml ? data.body.value : toPlain(data.body).value,
+ // Stop schedule when editing outbox messages and backup sendAt timestamp
+ let originalSendAt
+ if (type === 'outbox' && data.id && data.sendAt) {
+ originalSendAt = data.sendAt
+ const message = {
+ ...data,
+ body: data.isHtml ? data.body.value : toPlain(data.body).value,
+ }
+ await dispatch('outbox/stopMessage', { message })
}
- await dispatch('outbox/stopMessage', { message })
- }
-
- commit('showMessageComposer', {
- type,
- data,
- forwardedMessages,
- templateMessageId,
- originalSendAt,
+
+ commit('showMessageComposer', {
+ type,
+ data,
+ forwardedMessages,
+ templateMessageId,
+ originalSendAt,
+ })
})
},
async closeMessageComposer({ commit, dispatch, getters }, { restoreOriginalSendAt }) {
- // Restore original sendAt timestamp when requested
- const message = getters.composerMessage
- if (restoreOriginalSendAt && message.type === 'outbox' && message.options?.originalSendAt) {
- const body = message.data.body
- await dispatch('outbox/updateMessage', {
- id: message.data.id,
- message: {
- ...message.data,
- body: message.data.isHtml ? body.value : toPlain(body).value,
- sendAt: message.options.originalSendAt,
- },
- })
- }
+ return handleHttpAuthErrors(commit, async () => {
+ // Restore original sendAt timestamp when requested
+ const message = getters.composerMessage
+ if (restoreOriginalSendAt && message.type === 'outbox' && message.options?.originalSendAt) {
+ const body = message.data.body
+ await dispatch('outbox/updateMessage', {
+ id: message.data.id,
+ message: {
+ ...message.data,
+ body: message.data.isHtml ? body.value : toPlain(body).value,
+ sendAt: message.options.originalSendAt,
+ },
+ })
+ }
- commit('hideMessageComposer')
+ commit('hideMessageComposer')
+ })
},
async fetchEnvelope({ commit, getters }, id) {
- const cached = getters.getEnvelope(id)
- if (cached) {
- logger.debug(`using cached value for envelope ${id}`)
- return cached
- }
-
- const envelope = await fetchEnvelope(id)
- // Only commit if not undefined (not found)
- if (envelope) {
- commit('addEnvelope', {
- envelope,
- })
- }
+ return handleHttpAuthErrors(commit, async () => {
+ const cached = getters.getEnvelope(id)
+ if (cached) {
+ logger.debug(`using cached value for envelope ${id}`)
+ return cached
+ }
+
+ const envelope = await fetchEnvelope(id)
+ // Only commit if not undefined (not found)
+ if (envelope) {
+ commit('addEnvelope', {
+ envelope,
+ })
+ }
- // Always use the object from the store
- return getters.getEnvelope(id)
+ // Always use the object from the store
+ return getters.getEnvelope(id)
+ })
},
fetchEnvelopes({ state, commit, getters, dispatch }, { mailboxId, query, addToUnifiedMailboxes = true }) {
- const mailbox = getters.getMailbox(mailboxId)
+ return handleHttpAuthErrors(commit, async () => {
+ const mailbox = getters.getMailbox(mailboxId)
- if (mailbox.isUnified) {
- const fetchIndividualLists = pipe(
- map((mb) =>
- dispatch('fetchEnvelopes', {
- mailboxId: mb.databaseId,
- query,
- addToUnifiedMailboxes: false,
- })
- ),
- Promise.all.bind(Promise),
- andThen(map(sliceToPage))
- )
- const fetchUnifiedEnvelopes = pipe(
- findIndividualMailboxes(getters.getMailboxes, mailbox.specialRole),
- fetchIndividualLists,
- andThen(combineEnvelopeLists),
- andThen(sliceToPage),
+ if (mailbox.isUnified) {
+ const fetchIndividualLists = pipe(
+ map((mb) =>
+ dispatch('fetchEnvelopes', {
+ mailboxId: mb.databaseId,
+ query,
+ addToUnifiedMailboxes: false,
+ })
+ ),
+ Promise.all.bind(Promise),
+ andThen(map(sliceToPage))
+ )
+ const fetchUnifiedEnvelopes = pipe(
+ findIndividualMailboxes(getters.getMailboxes, mailbox.specialRole),
+ fetchIndividualLists,
+ andThen(combineEnvelopeLists),
+ andThen(sliceToPage),
+ andThen(
+ tap(
+ map((envelope) =>
+ commit('addEnvelope', {
+ envelope,
+ query,
+ })
+ )
+ )
+ )
+ )
+
+ return fetchUnifiedEnvelopes(getters.accounts)
+ }
+
+ return pipe(
+ fetchEnvelopes,
andThen(
tap(
map((envelope) =>
commit('addEnvelope', {
- envelope,
query,
+ envelope,
+ addToUnifiedMailboxes,
})
)
)
)
- )
-
- return fetchUnifiedEnvelopes(getters.accounts)
- }
-
- return pipe(
- fetchEnvelopes,
- andThen(
- tap(
- map((envelope) =>
- commit('addEnvelope', {
- query,
- envelope,
- addToUnifiedMailboxes,
- })
- )
- )
- )
- )(mailbox.accountId, mailboxId, query, undefined, PAGE_SIZE)
+ )(mailbox.accountId, mailboxId, query, undefined, PAGE_SIZE)
+ })
},
async fetchNextEnvelopePage({ commit, getters, dispatch }, { mailboxId, query }) {
- const envelopes = await dispatch('fetchNextEnvelopes', {
- mailboxId,
- query,
- quantity: PAGE_SIZE,
+ return handleHttpAuthErrors(commit, async () => {
+ const envelopes = await dispatch('fetchNextEnvelopes', {
+ mailboxId,
+ query,
+ quantity: PAGE_SIZE,
+ })
+ return envelopes
})
- return envelopes
},
async fetchNextEnvelopes({ commit, getters, dispatch }, { mailboxId, query, quantity, rec = true, addToUnifiedMailboxes = true }) {
- const mailbox = getters.getMailbox(mailboxId)
-
- if (mailbox.isUnified) {
- const getIndivisualLists = curry((query, m) => getters.getEnvelopes(m.databaseId, query))
- const individualCursor = curry((query, m) =>
- prop('dateInt', last(getters.getEnvelopes(m.databaseId, query)))
- )
- const cursor = individualCursor(query, mailbox)
+ return handleHttpAuthErrors(commit, async () => {
+ const mailbox = getters.getMailbox(mailboxId)
- if (cursor === undefined) {
- throw new Error('Unified list has no tail')
- }
- const nextLocalUnifiedEnvelopes = pipe(
- findIndividualMailboxes(getters.getMailboxes, mailbox.specialRole),
- map(getIndivisualLists(query)),
- combineEnvelopeLists,
- filter(
- where({
- dateInt: gt(cursor),
- })
- ),
- slice(0, quantity)
- )
- // We know the next envelopes based on local data
- // We have to fetch individual envelopes only if it ends in the known
- // next fetch. If it ended before, there is no data to fetch anyway. If
- // it ends after, we have all the relevant data already
- const needsFetch = curry((query, nextEnvelopes, mb) => {
- const c = individualCursor(query, mb)
- return nextEnvelopes.length < quantity || c >= head(nextEnvelopes).dateInt || c <= last(nextEnvelopes).dateInt
- })
+ if (mailbox.isUnified) {
+ const getIndivisualLists = curry((query, m) => getters.getEnvelopes(m.databaseId, query))
+ const individualCursor = curry((query, m) =>
+ prop('dateInt', last(getters.getEnvelopes(m.databaseId, query)))
+ )
+ const cursor = individualCursor(query, mailbox)
- const mailboxesToFetch = (accounts) =>
- pipe(
+ if (cursor === undefined) {
+ throw new Error('Unified list has no tail')
+ }
+ const nextLocalUnifiedEnvelopes = pipe(
findIndividualMailboxes(getters.getMailboxes, mailbox.specialRole),
- filter(needsFetch(query, nextLocalUnifiedEnvelopes(accounts)))
- )(accounts)
- const mbs = mailboxesToFetch(getters.accounts)
-
- if (rec && mbs.length) {
- logger.debug('not enough local envelopes for the next unified page. ' + mbs.length + ' fetches required', {
- mailboxes: mbs.map(mb => mb.databaseId),
- })
- return pipe(
- map((mb) =>
- dispatch('fetchNextEnvelopes', {
- mailboxId: mb.databaseId,
- query,
- quantity,
- addToUnifiedMailboxes: false,
+ map(getIndivisualLists(query)),
+ combineEnvelopeLists,
+ filter(
+ where({
+ dateInt: gt(cursor),
})
),
- Promise.all.bind(Promise),
- andThen(() =>
- dispatch('fetchNextEnvelopes', {
- mailboxId,
- query,
- quantity,
- rec: false,
- addToUnifiedMailboxes: false,
- })
- )
- )(mbs)
+ slice(0, quantity)
+ )
+ // We know the next envelopes based on local data
+ // We have to fetch individual envelopes only if it ends in the known
+ // next fetch. If it ended before, there is no data to fetch anyway. If
+ // it ends after, we have all the relevant data already
+ const needsFetch = curry((query, nextEnvelopes, mb) => {
+ const c = individualCursor(query, mb)
+ return nextEnvelopes.length < quantity || c >= head(nextEnvelopes).dateInt || c <= last(nextEnvelopes).dateInt
+ })
+
+ const mailboxesToFetch = (accounts) =>
+ pipe(
+ findIndividualMailboxes(getters.getMailboxes, mailbox.specialRole),
+ filter(needsFetch(query, nextLocalUnifiedEnvelopes(accounts)))
+ )(accounts)
+ const mbs = mailboxesToFetch(getters.accounts)
+
+ if (rec && mbs.length) {
+ logger.debug('not enough local envelopes for the next unified page. ' + mbs.length + ' fetches required', {
+ mailboxes: mbs.map(mb => mb.databaseId),
+ })
+ return pipe(
+ map((mb) =>
+ dispatch('fetchNextEnvelopes', {
+ mailboxId: mb.databaseId,
+ query,
+ quantity,
+ addToUnifiedMailboxes: false,
+ })
+ ),
+ Promise.all.bind(Promise),
+ andThen(() =>
+ dispatch('fetchNextEnvelopes', {
+ mailboxId,
+ query,
+ quantity,
+ rec: false,
+ addToUnifiedMailboxes: false,
+ })
+ )
+ )(mbs)
+ }
+
+ const envelopes = nextLocalUnifiedEnvelopes(getters.accounts)
+ logger.debug('next unified page can be built locally and consists of ' + envelopes.length + ' envelopes', { addToUnifiedMailboxes })
+ envelopes.map((envelope) =>
+ commit('addEnvelope', {
+ query,
+ envelope,
+ })
+ )
+ return envelopes
}
- const envelopes = nextLocalUnifiedEnvelopes(getters.accounts)
- logger.debug('next unified page can be built locally and consists of ' + envelopes.length + ' envelopes', { addToUnifiedMailboxes })
- envelopes.map((envelope) =>
- commit('addEnvelope', {
- query,
- envelope,
- })
- )
- return envelopes
- }
-
- const list = mailbox.envelopeLists[normalizedEnvelopeListId(query)]
- if (list === undefined) {
- console.warn("envelope list is not defined, can't fetch next envelopes", mailboxId, query)
- return Promise.resolve([])
- }
- const lastEnvelopeId = last(list)
- if (typeof lastEnvelopeId === 'undefined') {
- console.error('mailbox is empty', list)
- return Promise.reject(new Error('Local mailbox has no envelopes, cannot determine cursor'))
- }
- const lastEnvelope = getters.getEnvelope(lastEnvelopeId)
- if (typeof lastEnvelope === 'undefined') {
- return Promise.reject(new Error('Cannot find last envelope. Required for the mailbox cursor'))
- }
-
- return fetchEnvelopes(mailbox.accountId, mailboxId, query, lastEnvelope.dateInt, quantity).then((envelopes) => {
- logger.debug(`fetched ${envelopes.length} messages for mailbox ${mailboxId}`, {
- envelopes,
- addToUnifiedMailboxes,
- })
- envelopes.forEach((envelope) =>
- commit('addEnvelope', {
- query,
- envelope,
+ const list = mailbox.envelopeLists[normalizedEnvelopeListId(query)]
+ if (list === undefined) {
+ console.warn("envelope list is not defined, can't fetch next envelopes", mailboxId, query)
+ return Promise.resolve([])
+ }
+ const lastEnvelopeId = last(list)
+ if (typeof lastEnvelopeId === 'undefined') {
+ console.error('mailbox is empty', list)
+ return Promise.reject(new Error('Local mailbox has no envelopes, cannot determine cursor'))
+ }
+ const lastEnvelope = getters.getEnvelope(lastEnvelopeId)
+ if (typeof lastEnvelope === 'undefined') {
+ return Promise.reject(new Error('Cannot find last envelope. Required for the mailbox cursor'))
+ }
+
+ return fetchEnvelopes(mailbox.accountId, mailboxId, query, lastEnvelope.dateInt, quantity).then((envelopes) => {
+ logger.debug(`fetched ${envelopes.length} messages for mailbox ${mailboxId}`, {
+ envelopes,
addToUnifiedMailboxes,
})
- )
- return envelopes
+ envelopes.forEach((envelope) =>
+ commit('addEnvelope', {
+ query,
+ envelope,
+ addToUnifiedMailboxes,
+ })
+ )
+ return envelopes
+ })
})
},
syncEnvelopes({ commit, getters, dispatch }, { mailboxId, query, init = false }) {
- logger.debug(`starting mailbox sync of ${mailboxId} (${query})`)
+ return handleHttpAuthErrors(commit, async () => {
+ logger.debug(`starting mailbox sync of ${mailboxId} (${query})`)
- const mailbox = getters.getMailbox(mailboxId)
+ const mailbox = getters.getMailbox(mailboxId)
- if (mailbox.isUnified) {
- return Promise.all(
- getters.accounts
- .filter((account) => !account.isUnified)
- .map((account) =>
- Promise.all(
- getters
- .getMailboxes(account.id)
- .filter((mb) => mb.specialRole === mailbox.specialRole)
- .map((mailbox) =>
- dispatch('syncEnvelopes', {
- mailboxId: mailbox.databaseId,
- query,
- init,
- })
- )
+ if (mailbox.isUnified) {
+ return Promise.all(
+ getters.accounts
+ .filter((account) => !account.isUnified)
+ .map((account) =>
+ Promise.all(
+ getters
+ .getMailboxes(account.id)
+ .filter((mb) => mb.specialRole === mailbox.specialRole)
+ .map((mailbox) =>
+ dispatch('syncEnvelopes', {
+ mailboxId: mailbox.databaseId,
+ query,
+ init,
+ })
+ )
+ )
)
- )
- )
- } else if (mailbox.isPriorityInbox && query === undefined) {
- return Promise.all(
- getPrioritySearchQueries().map((query) => {
- return Promise.all(
- getters.accounts
- .filter((account) => !account.isUnified)
- .map((account) =>
- Promise.all(
- getters
- .getMailboxes(account.id)
- .filter((mb) => mb.specialRole === mailbox.specialRole)
- .map((mailbox) =>
- dispatch('syncEnvelopes', {
- mailboxId: mailbox.databaseId,
- query,
- init,
- })
- )
+ )
+ } else if (mailbox.isPriorityInbox && query === undefined) {
+ return Promise.all(
+ getPrioritySearchQueries().map((query) => {
+ return Promise.all(
+ getters.accounts
+ .filter((account) => !account.isUnified)
+ .map((account) =>
+ Promise.all(
+ getters
+ .getMailboxes(account.id)
+ .filter((mb) => mb.specialRole === mailbox.specialRole)
+ .map((mailbox) =>
+ dispatch('syncEnvelopes', {
+ mailboxId: mailbox.databaseId,
+ query,
+ init,
+ })
+ )
+ )
)
- )
- )
- })
- )
- }
+ )
+ })
+ )
+ }
- const ids = getters.getEnvelopes(mailboxId, query).map((env) => env.databaseId)
- logger.debug(`mailbox sync of ${mailboxId} (${query}) has ${ids.length} known IDs`)
- return syncEnvelopes(mailbox.accountId, mailboxId, ids, query, init)
- .then((syncData) => {
- logger.debug(`mailbox ${mailboxId} (${query}) synchronized, ${syncData.newMessages.length} new, ${syncData.changedMessages.length} changed and ${syncData.vanishedMessages.length} vanished messages`)
+ const ids = getters.getEnvelopes(mailboxId, query).map((env) => env.databaseId)
+ logger.debug(`mailbox sync of ${mailboxId} (${query}) has ${ids.length} known IDs`)
+ return syncEnvelopes(mailbox.accountId, mailboxId, ids, query, init)
+ .then((syncData) => {
+ logger.debug(`mailbox ${mailboxId} (${query}) synchronized, ${syncData.newMessages.length} new, ${syncData.changedMessages.length} changed and ${syncData.vanishedMessages.length} vanished messages`)
- const unifiedMailbox = getters.getUnifiedMailbox(mailbox.specialRole)
+ const unifiedMailbox = getters.getUnifiedMailbox(mailbox.specialRole)
- syncData.newMessages.forEach((envelope) => {
- commit('addEnvelope', {
- envelope,
- query,
+ syncData.newMessages.forEach((envelope) => {
+ commit('addEnvelope', {
+ envelope,
+ query,
+ })
+ if (unifiedMailbox) {
+ commit('updateEnvelope', {
+ envelope,
+ })
+ }
})
- if (unifiedMailbox) {
+ syncData.changedMessages.forEach((envelope) => {
commit('updateEnvelope', {
envelope,
})
- }
- })
- syncData.changedMessages.forEach((envelope) => {
- commit('updateEnvelope', {
- envelope,
})
- })
- syncData.vanishedMessages.forEach((id) => {
- commit('removeEnvelope', {
- id,
+ syncData.vanishedMessages.forEach((id) => {
+ commit('removeEnvelope', {
+ id,
+ })
+ // Already removed from unified inbox
+ })
+
+ commit('setMailboxUnreadCount', {
+ id: mailboxId,
+ unread: syncData.stats.unread,
})
- // Already removed from unified inbox
- })
- commit('setMailboxUnreadCount', {
- id: mailboxId,
- unread: syncData.stats.unread,
+ return syncData.newMessages
})
+ .catch((error) => {
+ return matchError(error, {
+ [SyncIncompleteError.getName()]() {
+ console.warn(`(initial) sync of mailbox ${mailboxId} (${query}) is incomplete, retriggering`)
+ return dispatch('syncEnvelopes', {
+ mailboxId,
+ query,
+ init,
+ })
+ },
+ [MailboxLockedError.getName()](error) {
+ if (init) {
+ logger.info('Sync failed because the mailbox is locked, stopping here because this is an initial sync', { error })
+ throw error
+ }
- return syncData.newMessages
- })
- .catch((error) => {
- return matchError(error, {
- [SyncIncompleteError.getName()]() {
- console.warn(`(initial) sync of mailbox ${mailboxId} (${query}) is incomplete, retriggering`)
- return dispatch('syncEnvelopes', { mailboxId, query, init })
- },
- [MailboxLockedError.getName()](error) {
- if (init) {
- logger.info('Sync failed because the mailbox is locked, stopping here because this is an initial sync', { error })
+ logger.info('Sync failed because the mailbox is locked, retriggering', { error })
+ return wait(1500).then(() => dispatch('syncEnvelopes', {
+ mailboxId,
+ query,
+ init,
+ }))
+ },
+ default(error) {
+ console.error('Could not sync envelopes: ' + error.message, error)
throw error
- }
-
- logger.info('Sync failed because the mailbox is locked, retriggering', { error })
- return wait(1500).then(() => dispatch('syncEnvelopes', { mailboxId, query, init }))
- },
- default(error) {
- console.error('Could not sync envelopes: ' + error.message, error)
- },
+ },
+ })
})
- })
+ })
},
- async syncInboxes({ getters, dispatch }) {
- const results = await Promise.all(
- getters.accounts
- .filter((a) => !a.isUnified)
- .map((account) => {
- return Promise.all(
- getters.getMailboxes(account.id).map(async (mailbox) => {
- if (mailbox.specialRole !== 'inbox') {
- return
- }
+ async syncInboxes({ commit, getters, dispatch }) {
+ return handleHttpAuthErrors(commit, async () => {
+ const results = await Promise.all(
+ getters.accounts
+ .filter((a) => !a.isUnified)
+ .map((account) => {
+ return Promise.all(
+ getters.getMailboxes(account.id).map(async (mailbox) => {
+ if (mailbox.specialRole !== 'inbox') {
+ return
+ }
+
+ const list = mailbox.envelopeLists[normalizedEnvelopeListId(undefined)]
+ if (list === undefined) {
+ await dispatch('fetchEnvelopes', {
+ mailboxId: mailbox.databaseId,
+ })
+ }
- const list = mailbox.envelopeLists[normalizedEnvelopeListId(undefined)]
- if (list === undefined) {
- await dispatch('fetchEnvelopes', {
+ return await dispatch('syncEnvelopes', {
mailboxId: mailbox.databaseId,
})
- }
-
- return await dispatch('syncEnvelopes', {
- mailboxId: mailbox.databaseId,
})
+ )
+ })
+ )
+ const newMessages = flatMapDeep(identity, results).filter((m) => m !== undefined)
+ if (newMessages.length === 0) {
+ return
+ }
+
+ try {
+ // Make sure the priority inbox is updated as well
+ logger.info('updating priority inbox')
+ for (const query of [priorityImportantQuery, priorityOtherQuery]) {
+ logger.info("sync'ing priority inbox section", { query })
+ const mailbox = getters.getMailbox(UNIFIED_INBOX_ID)
+ const list = mailbox.envelopeLists[normalizedEnvelopeListId(query)]
+ if (list === undefined) {
+ await dispatch('fetchEnvelopes', {
+ mailboxId: UNIFIED_INBOX_ID,
+ query,
})
- )
- })
- )
- const newMessages = flatMapDeep(identity, results).filter((m) => m !== undefined)
- if (newMessages.length === 0) {
- return
- }
-
- try {
- // Make sure the priority inbox is updated as well
- logger.info('updating priority inbox')
- for (const query of [priorityImportantQuery, priorityOtherQuery]) {
- logger.info("sync'ing priority inbox section", { query })
- const mailbox = getters.getMailbox(UNIFIED_INBOX_ID)
- const list = mailbox.envelopeLists[normalizedEnvelopeListId(query)]
- if (list === undefined) {
- await dispatch('fetchEnvelopes', {
+ }
+
+ await dispatch('syncEnvelopes', {
mailboxId: UNIFIED_INBOX_ID,
query,
})
}
-
- await dispatch('syncEnvelopes', {
- mailboxId: UNIFIED_INBOX_ID,
- query,
- })
+ } finally {
+ showNewMessagesNotification(newMessages)
}
- } finally {
- showNewMessagesNotification(newMessages)
- }
+ })
},
toggleEnvelopeFlagged({ commit, getters }, envelope) {
- // Change immediately and switch back on error
- const oldState = envelope.flags.flagged
- commit('flagEnvelope', {
- envelope,
- flag: 'flagged',
- value: !oldState,
- })
-
- setEnvelopeFlag(envelope.databaseId, 'flagged', !oldState).catch((e) => {
- console.error('could not toggle message flagged state', e)
-
- // Revert change
+ return handleHttpAuthErrors(commit, async () => {
+ // Change immediately and switch back on error
+ const oldState = envelope.flags.flagged
commit('flagEnvelope', {
envelope,
flag: 'flagged',
- value: oldState,
+ value: !oldState,
})
+
+ try {
+ await setEnvelopeFlag(envelope.databaseId, 'flagged', !oldState)
+ } catch (error) {
+ logger.error('Could not toggle message flagged state', { error })
+
+ // Revert change
+ commit('flagEnvelope', {
+ envelope,
+ flag: 'flagged',
+ value: oldState,
+ })
+
+ throw error
+ }
})
},
- async toggleEnvelopeImportant({ dispatch, getters }, envelope) {
- const importantLabel = '$label1'
- const hasTag = getters
- .getEnvelopeTags(envelope.databaseId)
- .some((tag) => tag.imapLabel === importantLabel)
- if (hasTag) {
- await dispatch('removeEnvelopeTag', {
- envelope,
- imapLabel: importantLabel,
- })
- } else {
- await dispatch('addEnvelopeTag', {
- envelope,
- imapLabel: importantLabel,
- })
- }
+ async toggleEnvelopeImportant({ commit, dispatch, getters }, envelope) {
+ return handleHttpAuthErrors(commit, async () => {
+ const importantLabel = '$label1'
+ const hasTag = getters
+ .getEnvelopeTags(envelope.databaseId)
+ .some((tag) => tag.imapLabel === importantLabel)
+ if (hasTag) {
+ await dispatch('removeEnvelopeTag', {
+ envelope,
+ imapLabel: importantLabel,
+ })
+ } else {
+ await dispatch('addEnvelopeTag', {
+ envelope,
+ imapLabel: importantLabel,
+ })
+ }
+ })
},
async toggleEnvelopeSeen({ commit, getters }, { envelope, seen }) {
- // Change immediately and switch back on error
- const oldState = envelope.flags.seen
- const newState = seen === undefined ? !oldState : seen
- commit('flagEnvelope', {
- envelope,
- flag: 'seen',
- value: newState,
- })
-
- try {
- await setEnvelopeFlag(envelope.databaseId, 'seen', newState)
- } catch (error) {
- console.error('could not toggle message seen state', error)
-
- // Revert change
+ return handleHttpAuthErrors(commit, async () => {
+ // Change immediately and switch back on error
+ const oldState = envelope.flags.seen
+ const newState = seen === undefined ? !oldState : seen
commit('flagEnvelope', {
envelope,
flag: 'seen',
- value: oldState,
+ value: newState,
})
- }
- },
- async toggleEnvelopeJunk({ commit, getters }, envelope) {
- // Change immediately and switch back on error
- const oldState = envelope.flags.$junk
- commit('flagEnvelope', {
- envelope,
- flag: '$junk',
- value: !oldState,
- })
- commit('flagEnvelope', {
- envelope,
- flag: '$notjunk',
- value: oldState,
- })
- try {
- await setEnvelopeFlag(envelope.databaseId, '$junk', !oldState)
- await setEnvelopeFlag(envelope.databaseId, '$notjunk', oldState)
- } catch (error) {
- console.error('could not toggle message junk state', error)
+ try {
+ await setEnvelopeFlag(envelope.databaseId, 'seen', newState)
+ } catch (error) {
+ console.error('could not toggle message seen state', error)
+
+ // Revert change
+ commit('flagEnvelope', {
+ envelope,
+ flag: 'seen',
+ value: oldState,
+ })
- // Revert change
+ throw error
+ }
+ })
+ },
+ async toggleEnvelopeJunk({ commit, getters }, envelope) {
+ return handleHttpAuthErrors(commit, async () => {
+ // Change immediately and switch back on error
+ const oldState = envelope.flags.$junk
commit('flagEnvelope', {
envelope,
flag: '$junk',
- value: oldState,
+ value: !oldState,
})
commit('flagEnvelope', {
envelope,
flag: '$notjunk',
- value: !oldState,
+ value: oldState,
})
- }
- },
- async markEnvelopeFavoriteOrUnfavorite({ commit, getters }, { envelope, favFlag }) {
- // Change immediately and switch back on error
- const oldState = envelope.flags.flagged
- commit('flagEnvelope', {
- envelope,
- flag: 'flagged',
- value: favFlag,
- })
- try {
- await setEnvelopeFlag(envelope.databaseId, 'flagged', favFlag)
- } catch (error) {
- console.error('could not favorite/unfavorite message ' + envelope.uid, error)
+ try {
+ await setEnvelopeFlag(envelope.databaseId, '$junk', !oldState)
+ await setEnvelopeFlag(envelope.databaseId, '$notjunk', oldState)
+ } catch (error) {
+ console.error('could not toggle message junk state', error)
+
+ // Revert change
+ commit('flagEnvelope', {
+ envelope,
+ flag: '$junk',
+ value: oldState,
+ })
+ commit('flagEnvelope', {
+ envelope,
+ flag: '$notjunk',
+ value: !oldState,
+ })
- // Revert change
+ throw error
+ }
+ })
+ },
+ async markEnvelopeFavoriteOrUnfavorite({ commit, getters }, { envelope, favFlag }) {
+ return handleHttpAuthErrors(commit, async () => {
+ // Change immediately and switch back on error
+ const oldState = envelope.flags.flagged
commit('flagEnvelope', {
envelope,
flag: 'flagged',
- value: oldState,
+ value: favFlag,
})
- }
+
+ try {
+ await setEnvelopeFlag(envelope.databaseId, 'flagged', favFlag)
+ } catch (error) {
+ console.error('could not favorite/unfavorite message ' + envelope.uid, error)
+
+ // Revert change
+ commit('flagEnvelope', {
+ envelope,
+ flag: 'flagged',
+ value: oldState,
+ })
+
+ throw error
+ }
+ })
},
- async markEnvelopeImportantOrUnimportant({ dispatch, getters }, { envelope, addTag }) {
- const importantLabel = '$label1'
- const hasTag = getters
- .getEnvelopeTags(envelope.databaseId)
- .some((tag) => tag.imapLabel === importantLabel)
- if (hasTag && !addTag) {
- await dispatch('removeEnvelopeTag', {
- envelope,
- imapLabel: importantLabel,
- })
- } else if (!hasTag && addTag) {
- await dispatch('addEnvelopeTag', {
- envelope,
- imapLabel: importantLabel,
- })
- }
+ async markEnvelopeImportantOrUnimportant({ commit, dispatch, getters }, { envelope, addTag }) {
+ return handleHttpAuthErrors(commit, async () => {
+ const importantLabel = '$label1'
+ const hasTag = getters
+ .getEnvelopeTags(envelope.databaseId)
+ .some((tag) => tag.imapLabel === importantLabel)
+ if (hasTag && !addTag) {
+ await dispatch('removeEnvelopeTag', {
+ envelope,
+ imapLabel: importantLabel,
+ })
+ } else if (!hasTag && addTag) {
+ await dispatch('addEnvelopeTag', {
+ envelope,
+ imapLabel: importantLabel,
+ })
+ }
+ })
},
async fetchThread({ getters, commit }, id) {
- const thread = await fetchThread(id)
- commit('addEnvelopeThread', {
- id,
- thread,
+ return handleHttpAuthErrors(commit, async () => {
+ const thread = await fetchThread(id)
+ commit('addEnvelopeThread', {
+ id,
+ thread,
+ })
+ return thread
})
- return thread
},
async fetchMessage({ getters, commit }, id) {
- const message = await fetchMessage(id)
- // Only commit if not undefined (not found)
- if (message) {
- commit('addMessage', {
- message,
- })
- }
- return message
+ return handleHttpAuthErrors(commit, async () => {
+ const message = await fetchMessage(id)
+ // Only commit if not undefined (not found)
+ if (message) {
+ commit('addMessage', {
+ message,
+ })
+ }
+ return message
+ })
},
async fetchItineraries({ commit }, id) {
- const itineraries = await fetchMessageItineraries(id)
- commit('addMessageItineraries', {
- id,
- itineraries,
+ return handleHttpAuthErrors(commit, async () => {
+ const itineraries = await fetchMessageItineraries(id)
+ commit('addMessageItineraries', {
+ id,
+ itineraries,
+ })
+ return itineraries
})
- return itineraries
},
async deleteMessage({ getters, commit }, { id }) {
- commit('removeEnvelope', { id })
+ return handleHttpAuthErrors(commit, async () => {
+ commit('removeEnvelope', { id })
- try {
- await deleteMessage(id)
- commit('removeMessage', { id })
- console.debug('message removed')
- } catch (err) {
- console.error('could not delete message', err)
- const envelope = getters.getEnvelope(id)
- if (envelope) {
- commit('addEnvelope', { envelope })
- } else {
- logger.error('could not find envelope', { id })
+ try {
+ await deleteMessage(id)
+ commit('removeMessage', { id })
+ console.debug('message removed')
+ } catch (err) {
+ console.error('could not delete message', err)
+ const envelope = getters.getEnvelope(id)
+ if (envelope) {
+ commit('addEnvelope', { envelope })
+ } else {
+ logger.error('could not find envelope', { id })
+ }
+ throw err
}
- throw err
- }
+ })
},
async createAlias({ commit }, { account, alias, name }) {
- const entity = await AliasService.createAlias(account.id, alias, name)
- commit('createAlias', {
- account,
- alias: entity,
+ return handleHttpAuthErrors(commit, async () => {
+ const entity = await AliasService.createAlias(account.id, alias, name)
+ commit('createAlias', {
+ account,
+ alias: entity,
+ })
})
},
async deleteAlias({ commit }, { account, aliasId }) {
- const entity = await AliasService.deleteAlias(account.id, aliasId)
- commit('deleteAlias', {
- account,
- aliasId: entity.id,
+ return handleHttpAuthErrors(commit, async () => {
+ const entity = await AliasService.deleteAlias(account.id, aliasId)
+ commit('deleteAlias', {
+ account,
+ aliasId: entity.id,
+ })
})
},
async updateAlias({ commit }, { account, aliasId, alias, name }) {
- const entity = await AliasService.updateAlias(account.id, aliasId, alias, name)
- commit('patchAlias', {
- account,
- aliasId: entity.id,
- data: { alias: entity.alias, name: entity.name },
+ return handleHttpAuthErrors(commit, async () => {
+ const entity = await AliasService.updateAlias(account.id, aliasId, alias, name)
+ commit('patchAlias', {
+ account,
+ aliasId: entity.id,
+ data: { alias: entity.alias, name: entity.name },
+ })
+ commit('editAccount', account)
})
- commit('editAccount', account)
},
async updateAliasSignature({ commit }, { account, aliasId, signature }) {
- const entity = await AliasService.updateSignature(account.id, aliasId, signature)
- commit('patchAlias', {
- account,
- aliasId: entity.id,
- data: { signature: entity.signature },
+ return handleHttpAuthErrors(commit, async () => {
+ const entity = await AliasService.updateSignature(account.id, aliasId, signature)
+ commit('patchAlias', {
+ account,
+ aliasId: entity.id,
+ data: { signature: entity.signature },
+ })
+ commit('editAccount', account)
})
- commit('editAccount', account)
},
async renameMailbox({ commit }, { account, mailbox, newName }) {
- const newMailbox = await patchMailbox(mailbox.databaseId, {
- name: newName,
- })
+ return handleHttpAuthErrors(commit, async () => {
+ const newMailbox = await patchMailbox(mailbox.databaseId, {
+ name: newName,
+ })
- console.debug(`mailbox ${mailbox.databaseId} renamed to ${newName}`, { mailbox })
- commit('removeMailbox', { id: mailbox.databaseId })
- commit('addMailbox', { account, mailbox: newMailbox })
+ console.debug(`mailbox ${mailbox.databaseId} renamed to ${newName}`, { mailbox })
+ commit('removeMailbox', { id: mailbox.databaseId })
+ commit('addMailbox', { account, mailbox: newMailbox })
+ })
},
async moveMessage({ commit }, { id, destMailboxId }) {
- await moveMessage(id, destMailboxId)
- commit('removeEnvelope', { id })
- commit('removeMessage', { id })
+ return handleHttpAuthErrors(commit, async () => {
+ await moveMessage(id, destMailboxId)
+ commit('removeEnvelope', { id })
+ commit('removeMessage', { id })
+ })
},
async fetchActiveSieveScript({ commit }, { accountId }) {
- const scriptData = await getActiveScript(accountId)
- commit('setActiveSieveScript', { accountId, scriptData })
+ return handleHttpAuthErrors(commit, async () => {
+ const scriptData = await getActiveScript(accountId)
+ commit('setActiveSieveScript', { accountId, scriptData })
+ })
},
async updateActiveSieveScript({ commit }, { accountId, scriptData }) {
- await updateActiveScript(accountId, scriptData)
- commit('setActiveSieveScript', { accountId, scriptData })
+ return handleHttpAuthErrors(commit, async () => {
+ await updateActiveScript(accountId, scriptData)
+ commit('setActiveSieveScript', { accountId, scriptData })
+ })
},
async updateSieveAccount({ commit }, { account, data }) {
- logger.debug(`update sieve settings for account ${account.id}`)
- try {
- await updateSieveAccount(account.id, data)
- commit('patchAccount', { account, data })
- } catch (error) {
- logger.error('failed to update sieve account: ', { error })
- throw error
- }
+ return handleHttpAuthErrors(commit, async () => {
+ logger.debug(`update sieve settings for account ${account.id}`)
+ try {
+ await updateSieveAccount(account.id, data)
+ commit('patchAccount', { account, data })
+ } catch (error) {
+ logger.error('failed to update sieve account: ', { error })
+ throw error
+ }
+ })
},
async createTag({ commit }, { displayName, color }) {
- const tag = await createEnvelopeTag(displayName, color)
- commit('addTag', { tag })
+ return handleHttpAuthErrors(commit, async () => {
+ const tag = await createEnvelopeTag(displayName, color)
+ commit('addTag', { tag })
+ })
},
async addEnvelopeTag({ commit, getters }, { envelope, imapLabel }) {
- // TODO: fetch tags indepently of envelopes and only send tag id here
- const tag = await setEnvelopeTag(envelope.databaseId, imapLabel)
- if (!getters.getTag(tag.id)) {
- commit('addTag', { tag })
- }
+ return handleHttpAuthErrors(commit, async () => {
+ // TODO: fetch tags indepently of envelopes and only send tag id here
+ const tag = await setEnvelopeTag(envelope.databaseId, imapLabel)
+ if (!getters.getTag(tag.id)) {
+ commit('addTag', { tag })
+ }
- commit('addEnvelopeTag', {
- envelope,
- tagId: tag.id,
+ commit('addEnvelopeTag', {
+ envelope,
+ tagId: tag.id,
+ })
})
},
async removeEnvelopeTag({ commit }, { envelope, imapLabel }) {
- const tag = await removeEnvelopeTag(envelope.databaseId, imapLabel)
- commit('removeEnvelopeTag', {
- envelope,
- tagId: tag.id,
+ return handleHttpAuthErrors(commit, async () => {
+ const tag = await removeEnvelopeTag(envelope.databaseId, imapLabel)
+ commit('removeEnvelopeTag', {
+ envelope,
+ tagId: tag.id,
+ })
})
},
async updateTag({ commit }, { tag, displayName, color }) {
- await updateEnvelopeTag(tag.id, displayName, color)
- commit('updateTag', { tag, displayName, color })
- logger.debug('tag updated', { tag, displayName, color })
+ return handleHttpAuthErrors(commit, async () => {
+ await updateEnvelopeTag(tag.id, displayName, color)
+ commit('updateTag', { tag, displayName, color })
+ logger.debug('tag updated', { tag, displayName, color })
+ })
},
async deleteThread({ getters, commit }, { envelope }) {
- commit('removeEnvelope', { id: envelope.databaseId })
-
- try {
- await ThreadService.deleteThread(envelope.databaseId)
- console.debug('thread removed')
- } catch (e) {
- commit('addEnvelope', envelope)
- console.error('could not delete thread', e)
- throw e
- }
+ return handleHttpAuthErrors(commit, async () => {
+ commit('removeEnvelope', { id: envelope.databaseId })
+
+ try {
+ await ThreadService.deleteThread(envelope.databaseId)
+ console.debug('thread removed')
+ } catch (e) {
+ commit('addEnvelope', envelope)
+ console.error('could not delete thread', e)
+ throw e
+ }
+ })
},
async moveThread({ getters, commit }, { envelope, destMailboxId }) {
- commit('removeEnvelope', { id: envelope.databaseId })
-
- try {
- await ThreadService.moveThread(envelope.databaseId, destMailboxId)
- console.debug('thread removed')
- } catch (e) {
- commit('addEnvelope', envelope)
- console.error('could not move thread', e)
- throw e
- }
+ return handleHttpAuthErrors(commit, async () => {
+ commit('removeEnvelope', { id: envelope.databaseId })
+
+ try {
+ await ThreadService.moveThread(envelope.databaseId, destMailboxId)
+ console.debug('thread removed')
+ } catch (e) {
+ commit('addEnvelope', envelope)
+ console.error('could not move thread', e)
+ throw e
+ }
+ })
},
/**
@@ -1068,8 +1189,10 @@ export default {
* @param {Function} context.commit Vuex store mutations
*/
async fetchCurrentUserPrincipal({ commit }) {
- await initializeClientForUserView()
- commit('setCurrentUserPrincipal', { currentUserPrincipal: getCurrentUserPrincipal() })
+ return handleHttpAuthErrors(commit, async () => {
+ await initializeClientForUserView()
+ commit('setCurrentUserPrincipal', { currentUserPrincipal: getCurrentUserPrincipal() })
+ })
},
/**
@@ -1080,9 +1203,11 @@ export default {
* @return {Promise<void>}
*/
async loadCollections({ commit }) {
- const { calendars } = await findAll()
- for (const calendar of calendars) {
- commit('addCalendar', { calendar })
- }
+ await handleHttpAuthErrors(commit, async () => {
+ const { calendars } = await findAll()
+ for (const calendar of calendars) {
+ commit('addCalendar', { calendar })
+ }
+ })
},
}
diff --git a/src/store/getters.js b/src/store/getters.js
index 56341e422..abc8fd2d1 100644
--- a/src/store/getters.js
+++ b/src/store/getters.js
@@ -30,6 +30,9 @@ export const getters = {
getPreference: (state) => (key, def) => {
return defaultTo(def, state.preferences[key])
},
+ isExpiredSession: (state) => {
+ return state.isExpiredSession
+ },
getAccount: (state) => (id) => {
return state.accounts[id]
},
diff --git a/src/store/index.js b/src/store/index.js
index e9fd46939..bd13f5d03 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -41,6 +41,7 @@ export default new Store({
root: {
namespaced: false,
state: {
+ isExpiredSession: false,
preferences: {},
accounts: {
[UNIFIED_ACCOUNT_ID]: {
diff --git a/src/store/mutations.js b/src/store/mutations.js
index b23931edc..80d180979 100644
--- a/src/store/mutations.js
+++ b/src/store/mutations.js
@@ -132,6 +132,9 @@ export default {
savePreference(state, { key, value }) {
Vue.set(state.preferences, key, value)
},
+ setSessionExpired(state) {
+ Vue.set(state, 'isExpiredSession', true)
+ },
addAccount(state, account) {
account.collapsed = account.collapsed ?? true
Vue.set(state.accounts, account.id, account)
diff --git a/src/tests/unit/App.spec.js b/src/tests/unit/App.spec.js
new file mode 100644
index 000000000..a33f7f645
--- /dev/null
+++ b/src/tests/unit/App.spec.js
@@ -0,0 +1,66 @@
+/*
+ * @copyright 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import { createLocalVue, shallowMount } from '@vue/test-utils'
+
+import App from '../../App'
+import Nextcloud from '../../mixins/Nextcloud'
+import Vuex from 'vuex'
+
+const localVue = createLocalVue()
+localVue.use(Vuex)
+localVue.mixin(Nextcloud)
+
+jest.mock('../../service/AutoConfigService')
+
+describe('App', () => {
+
+ let state
+ let getters
+ let store
+ let view
+
+ beforeEach(() => {
+ state = { isExpiredSession: false };
+ getters = {
+ isExpiredSession: (state) => state.isExpiredSession,
+ }
+ store = new Vuex.Store({
+ getters,
+ state,
+ })
+
+ view = shallowMount(App, {
+ store,
+ localVue,
+ })
+ })
+
+ it('handles session expiry', async() => {
+ // Stub and prevent the actual reload
+ view.vm.reload = jest.fn()
+
+ expect(view.vm.isExpiredSession).toBe(false)
+ state.isExpiredSession = true
+ expect(view.vm.isExpiredSession).toBe(true)
+ })
+
+})
diff --git a/src/tests/unit/http/sessionExpiryHandler.spec.js b/src/tests/unit/http/sessionExpiryHandler.spec.js
new file mode 100644
index 000000000..0bcbde213
--- /dev/null
+++ b/src/tests/unit/http/sessionExpiryHandler.spec.js
@@ -0,0 +1,82 @@
+/*
+ * @copyright 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import { handleHttpAuthErrors } from '../../../http/sessionExpiryHandler'
+
+describe('sessionExpiryHandler', () => {
+ it('does not influence successful requests', async () => {
+ const commit = jest.fn()
+
+ await handleHttpAuthErrors(commit, () => {})
+
+ expect(commit).not.toHaveBeenCalled()
+ })
+
+ it('ignores other 401s', async () => {
+ const commit = jest.fn()
+ let exception
+
+ try {
+ await handleHttpAuthErrors(commit, () => {
+ throw {
+ response: {
+ status: 401,
+ data: {
+ message: 'Bonjour',
+ },
+ },
+ }
+ })
+ } catch (e) {
+ exception = e
+ }
+
+ // Is this our exception?
+ expect(exception.response?.status === 401)
+ expect(commit).not.toHaveBeenCalled()
+ })
+
+ it('handles relevant 401s', async () => {
+ const commit = jest.fn()
+ let exception
+
+ try {
+ await handleHttpAuthErrors(commit, () => {
+ throw {
+ response: {
+ status: 401,
+ data: {
+ message: 'Current user is not logged in',
+ },
+ },
+ }
+ })
+ } catch (e) {
+ exception = e
+ }
+
+ // Is this our exception?
+ expect(exception.response?.status === 401)
+ expect(commit).toHaveBeenCalled()
+ })
+
+})
+
diff --git a/src/tests/unit/store/getters.spec.js b/src/tests/unit/store/getters.spec.js
index f049a5ff7..f8af1ecf6 100644
--- a/src/tests/unit/store/getters.spec.js
+++ b/src/tests/unit/store/getters.spec.js
@@ -31,6 +31,7 @@ describe('Vuex store getters', () => {
beforeEach(() => {
state = {
+ isExpiredSession: false,
accountList: [],
accounts: {},
mailboxes: {},
@@ -43,6 +44,11 @@ describe('Vuex store getters', () => {
bindGetters = () => mapObjIndexed(bindGetterToState(getters, state), getters)
})
+ it('gets session expiry', () => {
+ const getters = bindGetters()
+
+ expect(getters.isExpiredSession).toEqual(false)
+ })
it('gets all accounts', () => {
state.accountList.push('13')
state.accounts[13] = {