diff --git a/src/common/api/worker/facades/lazy/MailExportTokenFacade.ts b/src/common/api/worker/facades/lazy/MailExportTokenFacade.ts index 39daed6ff5e2..cff9edf20d7f 100644 --- a/src/common/api/worker/facades/lazy/MailExportTokenFacade.ts +++ b/src/common/api/worker/facades/lazy/MailExportTokenFacade.ts @@ -1,6 +1,7 @@ import { AccessExpiredError } from "../../../common/error/RestError.js" import { MailExportTokenService } from "../../../entities/tutanota/Services.js" import { IServiceExecutor } from "../../../common/ServiceRequest.js" +import { SuspensionBehavior } from "../../rest/RestClient" const TAG = "[MailExportTokenFacade]" @@ -65,7 +66,7 @@ export class MailExportTokenFacade { } this.currentExportToken = null - this.currentExportTokenRequest = this.serviceExecutor.post(MailExportTokenService, null).then( + this.currentExportTokenRequest = this.serviceExecutor.post(MailExportTokenService, null, { suspensionBehavior: SuspensionBehavior.Suspend }).then( (result) => { this.currentExportToken = result.mailExportToken as MailExportToken this.currentExportTokenRequest = null diff --git a/src/common/misc/TranslationKey.ts b/src/common/misc/TranslationKey.ts index 95945787e443..bee7a4567f12 100644 --- a/src/common/misc/TranslationKey.ts +++ b/src/common/misc/TranslationKey.ts @@ -1817,3 +1817,5 @@ export type TranslationKeyType = | "you_label" | "emptyString_msg" | "exportFinished_label" + | "exportErrorTooManyRequests_label" + | "exportErrorServiceUnavailable_label" diff --git a/src/mail-app/native/main/MailExportController.ts b/src/mail-app/native/main/MailExportController.ts index 2049bc08c8cd..23323fecdd58 100644 --- a/src/mail-app/native/main/MailExportController.ts +++ b/src/mail-app/native/main/MailExportController.ts @@ -3,22 +3,21 @@ import Stream from "mithril/stream" import stream from "mithril/stream" import { MailBag } from "../../../common/api/entities/tutanota/TypeRefs.js" import { GENERATED_MAX_ID, getElementId, isSameId } from "../../../common/api/common/utils/EntityUtils.js" -import { assertNotNull, delay, isNotNull, lastThrow, ofClass, promiseMap } from "@tutao/tutanota-utils" +import { assertNotNull, delay, isNotNull, lastThrow } from "@tutao/tutanota-utils" import { HtmlSanitizer } from "../../../common/misc/HtmlSanitizer.js" import { ExportFacade } from "../../../common/native/common/generatedipc/ExportFacade.js" import { LoginController } from "../../../common/api/main/LoginController.js" -import { FileController } from "../../../common/file/FileController.js" import { CancelledError } from "../../../common/api/common/error/CancelledError.js" -import { BulkMailLoader } from "../../workerUtils/index/BulkMailLoader.js" import { FileOpenError } from "../../../common/api/common/error/FileOpenError.js" import { isOfflineError } from "../../../common/api/common/utils/ErrorUtils.js" -import { NotFoundError } from "../../../common/api/common/error/RestError.js" +import { ServiceUnavailableError, TooManyRequestsError } from "../../../common/api/common/error/RestError.js" import { MailExportFacade } from "../../../common/api/worker/facades/lazy/MailExportFacade.js" +import type { TranslationText } from "../../../common/misc/LanguageViewModel" export type MailExportState = | { type: "idle" } | { type: "exporting"; mailboxDetail: MailboxDetail; progress: number; exportedMails: number } - | { type: "error"; message: string } + | { type: "error"; message: TranslationText } | { type: "finished" mailboxDetail: MailboxDetail @@ -127,8 +126,19 @@ export class MailExportController { private async runExport(mailboxDetail: MailboxDetail, mailBags: MailBag[], mailId: Id) { for (const mailBag of mailBags) { - await this.exportMailBag(mailBag, mailId) - if (this._state().type !== "exporting") { + try { + await this.exportMailBag(mailBag, mailId) + if (this._state().type !== "exporting") { + return + } + } catch (e) { + if (e instanceof TooManyRequestsError) { + this._state({ type: "error", message: "exportErrorTooManyRequests_label" }) + } else if (e instanceof ServiceUnavailableError) { + this._state({ type: "error", message: "exportErrorServiceUnavailable_label" }) + } else { + throw e + } return } } @@ -171,7 +181,7 @@ export class MailExportController { await this.exportFacade.saveMailboxExport(mailBundle, this.userId, mailBag._id, getElementId(mail)) } catch (e) { if (e instanceof FileOpenError) { - this._state({ type: "error", message: e.message }) + this._state({ type: "error", message: () => e.message }) return } else { throw e diff --git a/src/mail-app/settings/MailExportSettings.ts b/src/mail-app/settings/MailExportSettings.ts index c4ce25324f11..3b11da036b3e 100644 --- a/src/mail-app/settings/MailExportSettings.ts +++ b/src/mail-app/settings/MailExportSettings.ts @@ -114,7 +114,7 @@ export class MailExportSettings implements Component { case "error": return [ m(".flex-space-between.items-center.mt.mb-s", [ - m("small.noselect", state.message), + m("small.noselect", lang.getMaybeLazy(state.message)), m(Button, { label: "retry_action", click: () => { diff --git a/src/mail-app/translations/de.ts b/src/mail-app/translations/de.ts index 2395b30d7470..276357ad1430 100644 --- a/src/mail-app/translations/de.ts +++ b/src/mail-app/translations/de.ts @@ -1837,5 +1837,7 @@ export default { "yourMessage_label": "Deine Nachricht", "you_label": "Du", "exportFinished_label": "Export finished", + "exportErrorTooManyRequests_label": "Too many exports were requested recently", + "exportErrorServiceUnavailable_label": "Exporting is temporarily unavailable; please try again later" } } diff --git a/src/mail-app/translations/de_sie.ts b/src/mail-app/translations/de_sie.ts index d65b521a4699..29ef584e46be 100644 --- a/src/mail-app/translations/de_sie.ts +++ b/src/mail-app/translations/de_sie.ts @@ -1837,5 +1837,7 @@ export default { "yourMessage_label": "Ihre Nachricht", "you_label": "Sie", "exportFinished_label": "Export finished", + "exportErrorTooManyRequests_label": "Too many exports were requested recently", + "exportErrorServiceUnavailable_label": "Exporting is temporarily unavailable; please try again later" } } diff --git a/src/mail-app/translations/en.ts b/src/mail-app/translations/en.ts index 5f83347e7d91..8955c1b09f45 100644 --- a/src/mail-app/translations/en.ts +++ b/src/mail-app/translations/en.ts @@ -1833,5 +1833,7 @@ export default { "yourMessage_label": "Your message", "you_label": "You", "exportFinished_label": "Export finished", + "exportErrorTooManyRequests_label": "Too many exports were requested recently", + "exportErrorServiceUnavailable_label": "Exporting is temporarily unavailable; please try again later" } } diff --git a/test/tests/api/worker/facades/MailExportTokenFacadeTest.ts b/test/tests/api/worker/facades/MailExportTokenFacadeTest.ts index d2c73a355cfd..ba110a9321b7 100644 --- a/test/tests/api/worker/facades/MailExportTokenFacadeTest.ts +++ b/test/tests/api/worker/facades/MailExportTokenFacadeTest.ts @@ -1,5 +1,5 @@ import o from "@tutao/otest" -import { func, object, when } from "testdouble" +import { func, matchers, object, when } from "testdouble" import { createMailExportTokenServicePostOut } from "../../../../../src/common/api/entities/tutanota/TypeRefs" import { MailExportTokenService } from "../../../../../src/common/api/entities/tutanota/Services" import { AccessExpiredError, TooManyRequestsError } from "../../../../../src/common/api/common/error/RestError" @@ -23,7 +23,9 @@ o.spec("MailExportTokenFacade", () => { const expected = "result" const cb = func<(token: string) => Promise>() when(cb(validToken)).thenResolve(expected) - when(serviceExecutor.post(MailExportTokenService, null)).thenResolve(createMailExportTokenServicePostOut({ mailExportToken: validToken })) + when(serviceExecutor.post(MailExportTokenService, null, matchers.anything())).thenResolve( + createMailExportTokenServicePostOut({ mailExportToken: validToken }), + ) const result = await facade.loadWithToken(cb) @@ -47,7 +49,9 @@ o.spec("MailExportTokenFacade", () => { when(cb(validToken)).thenResolve(expected) when(cb(expiredToken)).thenReject(new AccessExpiredError("token expired")) facade._setCurrentExportToken(expiredToken) - when(serviceExecutor.post(MailExportTokenService, null)).thenResolve(createMailExportTokenServicePostOut({ mailExportToken: validToken })) + when(serviceExecutor.post(MailExportTokenService, null, matchers.anything())).thenResolve( + createMailExportTokenServicePostOut({ mailExportToken: validToken }), + ) const result = await facade.loadWithToken(cb) @@ -57,7 +61,7 @@ o.spec("MailExportTokenFacade", () => { o.test("when requesting token fails none are stored", async () => { const cb = func<(token: string) => Promise>() when(cb(expiredToken)).thenReject(new AccessExpiredError("token expired")) - when(serviceExecutor.post(MailExportTokenService, null)).thenReject(new TooManyRequestsError("no more tokens :(")) + when(serviceExecutor.post(MailExportTokenService, null, matchers.anything())).thenReject(new TooManyRequestsError("no more tokens :(")) await o(() => facade.loadWithToken(cb)).asyncThrows(TooManyRequestsError) diff --git a/test/tests/native/main/MailExportControllerTest.ts b/test/tests/native/main/MailExportControllerTest.ts index 7363bededf78..7042e8472136 100644 --- a/test/tests/native/main/MailExportControllerTest.ts +++ b/test/tests/native/main/MailExportControllerTest.ts @@ -26,6 +26,8 @@ import { createDataFile } from "../../../../src/common/api/common/DataFile.js" import { makeMailBundle } from "../../../../src/mail-app/mail/export/Bundler.js" import { MailboxExportState } from "../../../../src/common/desktop/export/MailboxExportPersistence.js" import { MailExportFacade } from "../../../../src/common/api/worker/facades/lazy/MailExportFacade.js" +import { ServiceUnavailableError, TooManyRequestsError } from "../../../../src/common/api/common/error/RestError" +import type { TranslationText } from "../../../../src/common/misc/LanguageViewModel" o.spec("MailExportController", function () { const userId = "userId" @@ -181,4 +183,21 @@ o.spec("MailExportController", function () { verify(exportFacade.endMailboxExport(userId)) }) }) + + o.spec("handle errors", function () { + o.test("TooManyRequestsError", async () => { + when(mailExportFacade.loadFixedNumberOfMailsWithCache(matchers.anything(), matchers.anything())).thenReject(new TooManyRequestsError(":(")) + await controller.startExport(mailboxDetail) + const currentState = controller.state() as { type: string; message: TranslationText } + o(currentState.type).equals("error") + o(currentState.message).equals("exportErrorTooManyRequests_label") + }) + o.test("ServiceUnavailableError", async () => { + when(mailExportFacade.loadFixedNumberOfMailsWithCache(matchers.anything(), matchers.anything())).thenReject(new ServiceUnavailableError(":(")) + await controller.startExport(mailboxDetail) + const currentState = controller.state() as { type: string; message: TranslationText } + o(currentState.type).equals("error") + o(currentState.message).equals("exportErrorServiceUnavailable_label") + }) + }) })