import { assertMainOrNode, isAndroidApp, isApp, isBrowser, isDesktop, isElectronClient, isIOSApp, isOfflineStorageAvailable, isTest, } from "../common/api/common/Env.js" import { EventController } from "../common/api/main/EventController.js" import { SearchModel } from "./search/model/SearchModel.js" import { type MailboxDetail, MailboxModel } from "../common/mailFunctionality/MailboxModel.js" import { MinimizedMailEditorViewModel } from "./mail/model/MinimizedMailEditorViewModel.js" import { ContactModel } from "../common/contactsFunctionality/ContactModel.js" import { EntityClient } from "../common/api/common/EntityClient.js" import { ProgressTracker } from "../common/api/main/ProgressTracker.js" import { CredentialsProvider } from "../common/misc/credentials/CredentialsProvider.js" import { bootstrapWorker, WorkerClient } from "../common/api/main/WorkerClient.js" import { CALENDAR_MIME_TYPE, FileController, guiDownload, MAIL_MIME_TYPES, VCARD_MIME_TYPES } from "../common/file/FileController.js" import { SecondFactorHandler } from "../common/misc/2fa/SecondFactorHandler.js" import { WebauthnClient } from "../common/misc/2fa/webauthn/WebauthnClient.js" import { LoginFacade } from "../common/api/worker/facades/LoginFacade.js" import { LoginController } from "../common/api/main/LoginController.js" import { AppHeaderAttrs, Header } from "../common/gui/Header.js" import { CustomerFacade } from "../common/api/worker/facades/lazy/CustomerFacade.js" import { GiftCardFacade } from "../common/api/worker/facades/lazy/GiftCardFacade.js" import { GroupManagementFacade } from "../common/api/worker/facades/lazy/GroupManagementFacade.js" import { ConfigurationDatabase } from "../common/api/worker/facades/lazy/ConfigurationDatabase.js" import { CalendarFacade } from "../common/api/worker/facades/lazy/CalendarFacade.js" import { MailFacade } from "../common/api/worker/facades/lazy/MailFacade.js" import { ShareFacade } from "../common/api/worker/facades/lazy/ShareFacade.js" import { CounterFacade } from "../common/api/worker/facades/lazy/CounterFacade.js" import { BookingFacade } from "../common/api/worker/facades/lazy/BookingFacade.js" import { MailAddressFacade } from "../common/api/worker/facades/lazy/MailAddressFacade.js" import { BlobFacade } from "../common/api/worker/facades/lazy/BlobFacade.js" import { UserManagementFacade } from "../common/api/worker/facades/lazy/UserManagementFacade.js" import { RecoverCodeFacade } from "../common/api/worker/facades/lazy/RecoverCodeFacade.js" import { ContactFacade } from "../common/api/worker/facades/lazy/ContactFacade.js" import { UsageTestController } from "@tutao/tutanota-usagetests" import { EphemeralUsageTestStorage, StorageBehavior, UsageTestModel } from "../common/misc/UsageTestModel.js" import { NewsModel } from "../common/misc/news/NewsModel.js" import { IServiceExecutor } from "../common/api/common/ServiceRequest.js" import { CryptoFacade } from "../common/api/worker/crypto/CryptoFacade.js" import { SearchTextInAppFacade } from "../common/native/common/generatedipc/SearchTextInAppFacade.js" import { SettingsFacade } from "../common/native/common/generatedipc/SettingsFacade.js" import { DesktopSystemFacade } from "../common/native/common/generatedipc/DesktopSystemFacade.js" import { WebMobileFacade } from "../common/native/main/WebMobileFacade.js" import { SystemPermissionHandler } from "../common/native/main/SystemPermissionHandler.js" import { InterWindowEventFacadeSendDispatcher } from "../common/native/common/generatedipc/InterWindowEventFacadeSendDispatcher.js" import { ExposedCacheStorage } from "../common/api/worker/rest/DefaultEntityRestCache.js" import { WorkerFacade } from "../common/api/worker/facades/WorkerFacade.js" import { PageContextLoginListener } from "../common/api/main/PageContextLoginListener.js" import { WebsocketConnectivityModel } from "../common/misc/WebsocketConnectivityModel.js" import { OperationProgressTracker } from "../common/api/main/OperationProgressTracker.js" import { InfoMessageHandler } from "../common/gui/InfoMessageHandler.js" import { NativeInterfaces } from "../common/native/main/NativeInterfaceFactory.js" import { EntropyFacade } from "../common/api/worker/facades/EntropyFacade.js" import { SqlCipherFacade } from "../common/native/common/generatedipc/SqlCipherFacade.js" import { assert, assertNotNull, defer, DeferredObject, lazy, lazyAsync, LazyLoaded, lazyMemoized, noOp } from "@tutao/tutanota-utils" import { RecipientsModel } from "../common/api/main/RecipientsModel.js" import { NoZoneDateProvider } from "../common/api/common/utils/NoZoneDateProvider.js" import { CalendarEvent, CalendarEventAttendee, Contact, Mail, MailboxProperties } from "../common/api/entities/tutanota/TypeRefs.js" import { SendMailModel } from "../common/mailFunctionality/SendMailModel.js" import { OfflineIndicatorViewModel } from "../common/gui/base/OfflineIndicatorViewModel.js" import { Router, ScopedRouter, ThrottledRouter } from "../common/gui/ScopedRouter.js" import { DeviceConfig, deviceConfig } from "../common/misc/DeviceConfig.js" import { InboxRuleHandler } from "./mail/model/InboxRuleHandler.js" import { SearchViewModel } from "./search/view/SearchViewModel.js" import { SearchRouter } from "../common/search/view/SearchRouter.js" import { MailOpenedListener } from "./mail/view/MailViewModel.js" import { getEnabledMailAddressesWithUser } from "../common/mailFunctionality/SharedMailUtils.js" import { Const, FeatureType, GroupType } from "../common/api/common/TutanotaConstants.js" import { ShareableGroupType } from "../common/sharing/GroupUtils.js" import { ReceivedGroupInvitationsModel } from "../common/sharing/model/ReceivedGroupInvitationsModel.js" import { CalendarViewModel } from "../calendar-app/calendar/view/CalendarViewModel.js" import { CalendarEventModel, CalendarOperation } from "../calendar-app/calendar/gui/eventeditor-model/CalendarEventModel.js" import { CalendarEventsRepository } from "../common/calendar/date/CalendarEventsRepository.js" import { showProgressDialog } from "../common/gui/dialogs/ProgressDialog.js" import { ContactSuggestionProvider, RecipientsSearchModel } from "../common/misc/RecipientsSearchModel.js" import { ConversationViewModel, ConversationViewModelFactory } from "./mail/view/ConversationViewModel.js" import { CreateMailViewerOptions } from "./mail/view/MailViewer.js" import { MailViewerViewModel } from "./mail/view/MailViewerViewModel.js" import { ExternalLoginViewModel } from "./mail/view/ExternalLoginView.js" import { NativeInterfaceMain } from "../common/native/main/NativeInterfaceMain.js" import { NativeFileApp } from "../common/native/common/FileApp.js" import type { NativePushServiceApp } from "../common/native/main/NativePushServiceApp.js" import { CommonSystemFacade } from "../common/native/common/generatedipc/CommonSystemFacade.js" import { ThemeFacade } from "../common/native/common/generatedipc/ThemeFacade.js" import { MobileSystemFacade } from "../common/native/common/generatedipc/MobileSystemFacade.js" import { MobileContactsFacade } from "../common/native/common/generatedipc/MobileContactsFacade.js" import { NativeCredentialsFacade } from "../common/native/common/generatedipc/NativeCredentialsFacade.js" import { MailAddressNameChanger, MailAddressTableModel, UserInfo } from "../common/settings/mailaddress/MailAddressTableModel.js" import { DrawerMenuAttrs } from "../common/gui/nav/DrawerMenu.js" import { DomainConfigProvider } from "../common/api/common/DomainConfigProvider.js" import { CredentialRemovalHandler } from "../common/login/CredentialRemovalHandler.js" import { LoginViewModel } from "../common/login/LoginViewModel.js" import { ProgrammingError } from "../common/api/common/error/ProgrammingError.js" import { EntropyCollector } from "../common/api/main/EntropyCollector.js" import { notifications } from "../common/gui/Notifications.js" import { windowFacade } from "../common/misc/WindowFacade.js" import { BrowserWebauthn } from "../common/misc/2fa/webauthn/BrowserWebauthn.js" import { FileControllerBrowser } from "../common/file/FileControllerBrowser.js" import { FileControllerNative } from "../common/file/FileControllerNative.js" import { CalendarInfo, CalendarModel } from "../calendar-app/calendar/model/CalendarModel.js" import { CalendarInviteHandler } from "../calendar-app/calendar/view/CalendarInvites.js" import { AlarmScheduler } from "../common/calendar/date/AlarmScheduler.js" import { SchedulerImpl } from "../common/api/common/utils/Scheduler.js" import type { CalendarEventPreviewViewModel } from "../calendar-app/calendar/gui/eventpopup/CalendarEventPreviewViewModel.js" import { isCustomizationEnabledForCustomer } from "../common/api/common/utils/CustomerUtils.js" import { NativeContactsSyncManager } from "./contacts/model/NativeContactsSyncManager.js" import { PostLoginActions } from "../common/login/PostLoginActions.js" import { CredentialFormatMigrator } from "../common/misc/credentials/CredentialFormatMigrator.js" import { AddNotificationEmailDialog } from "./settings/AddNotificationEmailDialog.js" import { NativeThemeFacade, ThemeController, WebThemeFacade } from "../common/gui/ThemeController.js" import { HtmlSanitizer } from "../common/misc/HtmlSanitizer.js" import { theme } from "../common/gui/theme.js" import { SearchIndexStateInfo } from "../common/api/worker/search/SearchTypes.js" import { MobilePaymentsFacade } from "../common/native/common/generatedipc/MobilePaymentsFacade.js" import { MAIL_PREFIX } from "../common/misc/RouteChange.js" import { getDisplayedSender } from "../common/api/common/CommonMailUtils.js" import { MailModel } from "./mail/model/MailModel.js" import type { CommonLocator } from "../common/api/main/CommonLocator.js" import { WorkerRandomizer } from "../common/api/worker/workerInterfaces.js" import { WorkerInterface } from "./workerUtils/worker/WorkerImpl.js" import { isMailInSpamOrTrash } from "./mail/model/MailChecks.js" import type { ContactImporter } from "./contacts/ContactImporter.js" import { ExternalCalendarFacade } from "../common/native/common/generatedipc/ExternalCalendarFacade.js" import { AppType } from "../common/misc/ClientConstants.js" import { ParsedEvent } from "../common/calendar/gui/CalendarImporter.js" import type { CalendarContactPreviewViewModel } from "../calendar-app/calendar/gui/eventpopup/CalendarContactPreviewViewModel.js" import { KeyLoaderFacade } from "../common/api/worker/facades/KeyLoaderFacade.js" import { KeyVerificationFacade } from "../common/api/worker/facades/lazy/KeyVerificationFacade" import { ContactSuggestion } from "../common/native/common/generatedipc/ContactSuggestion" import { MailImporter } from "./mail/import/MailImporter.js" import type { MailExportController } from "./native/main/MailExportController.js" import { ExportFacade } from "../common/native/common/generatedipc/ExportFacade.js" import { BulkMailLoader } from "./workerUtils/index/BulkMailLoader.js" import { MailExportFacade } from "../common/api/worker/facades/lazy/MailExportFacade.js" import { SyncTracker } from "../common/api/main/SyncTracker.js" import { Indexer } from "./workerUtils/index/Indexer" import { SearchFacade } from "./workerUtils/index/SearchFacade" import { getEventWithDefaultTimes, setNextHalfHour } from "../common/api/common/utils/CommonCalendarUtils.js" import { ClientModelInfo, ClientTypeModelResolver } from "../common/api/common/EntityFunctions" import { OfflineStorageSettingsModel } from "../common/offline/OfflineStorageSettingsModel" import { SearchToken } from "../common/api/common/utils/QueryTokenUtils" import type { ContactSearchFacade } from "./workerUtils/index/ContactSearchFacade" import { PublicEncryptionKeyProvider } from "../common/api/worker/facades/PublicEncryptionKeyProvider" import { IdentityKeyCreator } from "../common/api/worker/facades/lazy/IdentityKeyCreator" import { PublicIdentityKeyProvider } from "../common/api/worker/facades/PublicIdentityKeyProvider" import { WhitelabelThemeGenerator } from "../common/gui/WhitelabelThemeGenerator" import { UndoModel } from "./UndoModel" import { GroupSettingsModel } from "../common/sharing/model/GroupSettingsModel" import { AutosaveFacade } from "../common/api/worker/facades/lazy/AutosaveFacade" import { lang } from "../common/misc/LanguageViewModel.js" import { SpamClassificationHandler } from "./workerUtils/spamClassification/SpamClassificationHandler" import { SpamClassifier } from "./workerUtils/spamClassification/SpamClassifier" assertMainOrNode() class MailLocator implements CommonLocator { eventController!: EventController search!: SearchModel mailboxModel!: MailboxModel mailModel!: MailModel minimizedMailModel!: MinimizedMailEditorViewModel contactModel!: ContactModel entityClient!: EntityClient progressTracker!: ProgressTracker credentialsProvider!: CredentialsProvider worker!: WorkerClient fileController!: FileController secondFactorHandler!: SecondFactorHandler webAuthn!: WebauthnClient loginFacade!: LoginFacade logins!: LoginController header!: Header customerFacade!: CustomerFacade keyLoaderFacade!: KeyLoaderFacade giftCardFacade!: GiftCardFacade groupManagementFacade!: GroupManagementFacade identityKeyCreator!: IdentityKeyCreator configFacade!: ConfigurationDatabase calendarFacade!: CalendarFacade mailFacade!: MailFacade shareFacade!: ShareFacade counterFacade!: CounterFacade indexerFacade!: Indexer searchFacade!: SearchFacade contactSearchFacade!: ContactSearchFacade bookingFacade!: BookingFacade mailAddressFacade!: MailAddressFacade keyVerificationFacade!: KeyVerificationFacade publicEncryptionKeyProvider!: PublicEncryptionKeyProvider publicIdentityKeyProvider!: PublicIdentityKeyProvider blobFacade!: BlobFacade userManagementFacade!: UserManagementFacade recoverCodeFacade!: RecoverCodeFacade contactFacade!: ContactFacade usageTestController!: UsageTestController usageTestModel!: UsageTestModel newsModel!: NewsModel serviceExecutor!: IServiceExecutor cryptoFacade!: CryptoFacade searchTextFacade!: SearchTextInAppFacade desktopSettingsFacade!: SettingsFacade desktopSystemFacade!: DesktopSystemFacade exportFacade!: ExportFacade webMobileFacade!: WebMobileFacade systemPermissionHandler!: SystemPermissionHandler interWindowEventSender!: InterWindowEventFacadeSendDispatcher cacheStorage!: ExposedCacheStorage workerFacade!: WorkerFacade loginListener!: PageContextLoginListener random!: WorkerRandomizer connectivityModel!: WebsocketConnectivityModel operationProgressTracker!: OperationProgressTracker infoMessageHandler!: InfoMessageHandler themeController!: ThemeController Const!: Record bulkMailLoader!: BulkMailLoader mailExportFacade!: MailExportFacade syncTracker!: SyncTracker spamClassifier: SpamClassifier | null = null whitelabelThemeGenerator!: WhitelabelThemeGenerator autosaveFacade!: AutosaveFacade private nativeInterfaces: NativeInterfaces | null = null private mailImporter: MailImporter | null = null private entropyFacade!: EntropyFacade private sqlCipherFacade!: SqlCipherFacade readonly typeModelResolver: lazy = lazyMemoized(() => { return ClientModelInfo.getInstance() }) readonly recipientsModel: lazyAsync = lazyMemoized(async () => { const { RecipientsModel } = await import("../common/api/main/RecipientsModel.js") return new RecipientsModel(this.contactModel, this.logins, this.mailFacade, this.entityClient) }) async noZoneDateProvider(): Promise { return new NoZoneDateProvider() } async sendMailModel(mailboxDetails: MailboxDetail, mailboxProperties: MailboxProperties): Promise { const factory = await this.sendMailModelSyncFactory(mailboxDetails, mailboxProperties) return factory() } private readonly redraw: lazyAsync<() => unknown> = lazyMemoized(async () => { const m = await import("mithril") return m.redraw }) readonly offlineIndicatorViewModel = lazyMemoized(async () => { return new OfflineIndicatorViewModel( this.cacheStorage, this.loginListener, this.connectivityModel, this.logins, this.progressTracker, await this.redraw(), ) }) async appHeaderAttrs(): Promise { return { offlineIndicatorModel: await this.offlineIndicatorViewModel(), newsModel: this.newsModel, } } readonly mailViewModel = lazyMemoized(async () => { const { MailViewModel } = await import("../mail-app/mail/view/MailViewModel.js") const conversationViewModelFactory = await this.conversationViewModelFactory() const router = new ScopedRouter(this.throttledRouter(), "/mail") return new MailViewModel( this.mailboxModel, this.mailModel, this.entityClient, this.eventController, this.connectivityModel, this.cacheStorage, conversationViewModelFactory, this.mailOpenedListener, deviceConfig, this.inboxRuleHandler(), router, await this.redraw(), ) }) readonly affiliateViewModel = lazyMemoized(async () => { const { AffiliateViewModel } = await import("../common/settings/AffiliateViewModel.js") return new AffiliateViewModel() }) readonly inboxRuleHandler = lazyMemoized(() => { return new InboxRuleHandler(this.mailFacade, this.logins, this.mailModel) }) readonly spamClassificationHandler = lazyMemoized(() => { return new SpamClassificationHandler(this.mailFacade, this.spamClassifier) }) async searchViewModelFactory(): Promise<() => SearchViewModel> { const { SearchViewModel } = await import("../mail-app/search/view/SearchViewModel.js") const conversationViewModelFactory = await this.conversationViewModelFactory() const redraw = await this.redraw() const searchRouter = await this.scopedSearchRouter() const calendarEventsRepository = await this.calendarEventsRepository() const offlineStorageSettings = await this.offlineStorageSettingsModel() const calendarModel = await this.calendarModel() return () => { return new SearchViewModel( searchRouter, this.search, this.searchFacade, this.mailboxModel, this.logins, this.indexerFacade, this.entityClient, this.eventController, this.mailOpenedListener, this.calendarFacade, this.progressTracker, conversationViewModelFactory, calendarEventsRepository, calendarModel, redraw, deviceConfig.getMailAutoSelectBehavior(), offlineStorageSettings, ) } } readonly throttledRouter: lazy = lazyMemoized(() => new ThrottledRouter()) readonly scopedSearchRouter: lazyAsync = lazyMemoized(async () => { const { SearchRouter } = await import("../common/search/view/SearchRouter.js") return new SearchRouter(new ScopedRouter(this.throttledRouter(), "/search")) }) readonly unscopedSearchRouter: lazyAsync = lazyMemoized(async () => { const { SearchRouter } = await import("../common/search/view/SearchRouter.js") return new SearchRouter(this.throttledRouter()) }) readonly mailOpenedListener: MailOpenedListener = { onEmailOpened: isDesktop() ? (mail) => { this.desktopSystemFacade.sendSocketMessage(getDisplayedSender(mail).address) } : noOp, } readonly contactViewModel = lazyMemoized(async () => { const { ContactViewModel } = await import("../mail-app/contacts/view/ContactViewModel.js") const router = new ScopedRouter(this.throttledRouter(), "/contact") return new ContactViewModel(this.contactModel, this.entityClient, this.eventController, router, await this.redraw()) }) readonly contactListViewModel = lazyMemoized(async () => { const { ContactListViewModel } = await import("../mail-app/contacts/view/ContactListViewModel.js") const router = new ScopedRouter(this.throttledRouter(), "/contactlist") return new ContactListViewModel( this.entityClient, this.groupManagementFacade, this.logins, this.eventController, this.contactModel, await this.receivedGroupInvitationsModel(GroupType.ContactList), router, this.groupSettingsModel, await this.redraw(), ) }) readonly groupSettingsModel: lazy> = lazyMemoized(async () => { const { GroupSettingsModel } = await import("../common/sharing/model/GroupSettingsModel.js") return new GroupSettingsModel(this.entityClient, this.logins) }) async receivedGroupInvitationsModel(groupType: TypeOfGroup): Promise> { const { ReceivedGroupInvitationsModel } = await import("../common/sharing/model/ReceivedGroupInvitationsModel.js") return new ReceivedGroupInvitationsModel(groupType, this.eventController, this.entityClient, this.logins) } readonly calendarViewModel = lazyMemoized>(async () => { const { CalendarViewModel } = await import("../calendar-app/calendar/view/CalendarViewModel.js") const { DefaultDateProvider } = await import("../common/calendar/date/CalendarUtils") const timeZone = new DefaultDateProvider().timeZone() return new CalendarViewModel( this.logins, async (mode: CalendarOperation, event: CalendarEvent) => { const mailboxDetail = await this.mailboxModel.getUserMailboxDetails() const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetail.mailboxGroupRoot) return await this.calendarEventModel(mode, event, mailboxDetail, mailboxProperties, null) }, (...args) => this.calendarEventPreviewModel(...args), (...args) => this.calendarContactPreviewModel(...args), await this.calendarModel(), await this.calendarEventsRepository(), this.entityClient, this.eventController, this.progressTracker, deviceConfig, await this.receivedGroupInvitationsModel(GroupType.Calendar), timeZone, this.mailboxModel, this.contactModel, this.groupSettingsModel, ) }) readonly calendarEventsRepository: lazyAsync = lazyMemoized(async () => { const { CalendarEventsRepository } = await import("../common/calendar/date/CalendarEventsRepository.js") const { DefaultDateProvider } = await import("../common/calendar/date/CalendarUtils") const timeZone = new DefaultDateProvider().timeZone() return new CalendarEventsRepository( await this.calendarModel(), this.calendarFacade, timeZone, this.entityClient, this.eventController, this.contactModel, this.logins, ) }) /** This ugly bit exists because CalendarEventWhoModel wants a sync factory. */ private async sendMailModelSyncFactory(mailboxDetails: MailboxDetail, mailboxProperties: MailboxProperties): Promise<() => SendMailModel> { const { SendMailModel } = await import("../common/mailFunctionality/SendMailModel.js") const recipientsModel = await this.recipientsModel() const dateProvider = await this.noZoneDateProvider() return () => new SendMailModel( this.mailFacade, this.entityClient, this.logins, this.mailboxModel, this.contactModel, this.eventController, mailboxDetails, recipientsModel, dateProvider, mailboxProperties, this.autosaveFacade, async (mail: Mail) => { return await isMailInSpamOrTrash(mail, mailLocator.mailModel) }, this.syncTracker, ) } async calendarEventModel( editMode: CalendarOperation, event: Partial, mailboxDetail: MailboxDetail, mailboxProperties: MailboxProperties, responseTo: Mail | null, ): Promise { const [{ makeCalendarEventModel }, { getTimeZone }, { calendarNotificationSender }] = await Promise.all([ import("../calendar-app/calendar/gui/eventeditor-model/CalendarEventModel.js"), import("../common/calendar/date/CalendarUtils.js"), import("../calendar-app/calendar/view/CalendarNotificationSender.js"), ]) const sendMailModelFactory = await this.sendMailModelSyncFactory(mailboxDetail, mailboxProperties) const showProgress = (p: Promise) => showProgressDialog("pleaseWait_msg", p) return await makeCalendarEventModel( editMode, event, await this.recipientsModel(), await this.calendarModel(), this.logins, mailboxDetail, mailboxProperties, sendMailModelFactory, calendarNotificationSender, this.entityClient, responseTo, getTimeZone(), showProgress, ) } async recipientsSearchModel(): Promise { const { RecipientsSearchModel } = await import("../common/misc/RecipientsSearchModel.js") const suggestionsProvider = await this.contactSuggestionProvider() return new RecipientsSearchModel(await this.recipientsModel(), this.contactModel, suggestionsProvider, this.entityClient) } private async contactSuggestionProvider(): Promise { if (isApp()) { const { MobileContactSuggestionProvider } = await import("../common/native/main/MobileContactSuggestionProvider.js") return new MobileContactSuggestionProvider(this.mobileContactsFacade) } else { return { async getContactSuggestions(_query: string): Promise { return [] }, } } } readonly conversationViewModelFactory: lazyAsync = async () => { const { ConversationViewModel } = await import("../mail-app/mail/view/ConversationViewModel.js") const factory = await this.mailViewerViewModelFactory() const m = await import("mithril") return (options: CreateMailViewerOptions) => { return new ConversationViewModel( options, (options) => factory(options), this.entityClient, this.eventController, deviceConfig, this.mailModel, m.redraw, ) } } async conversationViewModel(options: CreateMailViewerOptions): Promise { const factory = await this.conversationViewModelFactory() return factory(options) } contactImporter = async (): Promise => { const { ContactImporter } = await import("../mail-app/contacts/ContactImporter.js") return new ContactImporter( this.contactFacade, this.systemPermissionHandler, isApp() ? this.mobileContactsFacade : null, isApp() ? this.nativeContactsSyncManager() : null, ) } async mailViewerViewModelFactory(): Promise<(options: CreateMailViewerOptions) => MailViewerViewModel> { const { MailViewerViewModel } = await import("../mail-app/mail/view/MailViewerViewModel.js") const eventRepository = await this.calendarEventsRepository() const undoModel = await this.undoModel() return ({ mail, showFolder, highlightedTokens }) => new MailViewerViewModel( mail, showFolder, this.entityClient, this.mailboxModel, this.mailModel, isBrowser() ? null : this.commonSystemFacade, this.contactModel, this.configFacade, this.fileController, this.logins, this.eventController, this.workerFacade, this.search, this.mailFacade, this.cryptoFacade, () => this.contactImporter(), highlightedTokens ?? [], eventRepository, undoModel, ) } async externalLoginViewModelFactory(): Promise<() => ExternalLoginViewModel> { const { ExternalLoginViewModel } = await import("./mail/view/ExternalLoginView.js") return () => new ExternalLoginViewModel(this.credentialsProvider) } get deviceConfig(): DeviceConfig { return deviceConfig } get native(): NativeInterfaceMain { return this.getNativeInterface("native") } get fileApp(): NativeFileApp { return this.getNativeInterface("fileApp") } get pushService(): NativePushServiceApp { return this.getNativeInterface("pushService") } get commonSystemFacade(): CommonSystemFacade { return this.getNativeInterface("commonSystemFacade") } get themeFacade(): ThemeFacade { return this.getNativeInterface("themeFacade") } get externalCalendarFacade(): ExternalCalendarFacade { return this.getNativeInterface("externalCalendarFacade") } get systemFacade(): MobileSystemFacade { return this.getNativeInterface("mobileSystemFacade") } get mobileContactsFacade(): MobileContactsFacade { return this.getNativeInterface("mobileContactsFacade") } get nativeCredentialsFacade(): NativeCredentialsFacade { return this.getNativeInterface("nativeCredentialsFacade") } get mobilePaymentsFacade(): MobilePaymentsFacade { return this.getNativeInterface("mobilePaymentsFacade") } async mailAddressTableModelForOwnMailbox(): Promise { const { MailAddressTableModel } = await import("../common/settings/mailaddress/MailAddressTableModel.js") const nameChanger = await this.ownMailAddressNameChanger() return new MailAddressTableModel( this.entityClient, this.serviceExecutor, this.mailAddressFacade, this.logins, this.eventController, { user: this.logins.getUserController().user, userGroupInfo: this.logins.getUserController().userGroupInfo }, nameChanger, await this.redraw(), ) } async mailAddressTableModelForAdmin(mailGroupId: Id, userId: Id, userInfo: UserInfo): Promise { const { MailAddressTableModel } = await import("../common/settings/mailaddress/MailAddressTableModel.js") const nameChanger = await this.adminNameChanger(mailGroupId, userId) return new MailAddressTableModel( this.entityClient, this.serviceExecutor, this.mailAddressFacade, this.logins, this.eventController, userInfo, nameChanger, await this.redraw(), ) } async ownMailAddressNameChanger(): Promise { const { OwnMailAddressNameChanger } = await import("../common/settings/mailaddress/OwnMailAddressNameChanger.js") return new OwnMailAddressNameChanger(this.mailboxModel, this.entityClient) } async adminNameChanger(mailGroupId: Id, userId: Id): Promise { const { AnotherUserMailAddressNameChanger } = await import("../common/settings/mailaddress/AnotherUserMailAddressNameChanger.js") return new AnotherUserMailAddressNameChanger(this.mailAddressFacade, mailGroupId, userId) } async drawerAttrsFactory(): Promise<() => DrawerMenuAttrs> { return () => ({ logins: this.logins, newsModel: this.newsModel, desktopSystemFacade: this.desktopSystemFacade, }) } domainConfigProvider(): DomainConfigProvider { return new DomainConfigProvider() } async credentialsRemovalHandler(): Promise { const { NoopCredentialRemovalHandler, AppsCredentialRemovalHandler } = await import("../common/login/CredentialRemovalHandler.js") return isBrowser() ? new NoopCredentialRemovalHandler() : new AppsCredentialRemovalHandler(this.pushService, this.configFacade, async (login, userId) => { if (isApp()) { await mailLocator.nativeContactsSyncManager().disableSync(userId, login) } await mailLocator.indexerFacade.deleteIndex(userId) if (isDesktop()) { await mailLocator.exportFacade.clearExportState(userId) } }) } async loginViewModelFactory(): Promise> { const { LoginViewModel } = await import("../common/login/LoginViewModel.js") const credentialsRemovalHandler = await mailLocator.credentialsRemovalHandler() const { MobileAppLock, NoOpAppLock } = await import("../common/login/AppLock.js") const appLock = isApp() ? new MobileAppLock(assertNotNull(this.nativeInterfaces).mobileSystemFacade, assertNotNull(this.nativeInterfaces).nativeCredentialsFacade) : new NoOpAppLock() return () => { const domainConfig = isBrowser() ? mailLocator.domainConfigProvider().getDomainConfigForHostname(location.hostname, location.protocol, location.port) : // in this case, we know that we have a staticUrl set that we need to use mailLocator.domainConfigProvider().getCurrentDomainConfig() return new LoginViewModel( mailLocator.logins, mailLocator.credentialsProvider, mailLocator.secondFactorHandler, deviceConfig, domainConfig, credentialsRemovalHandler, isBrowser() ? null : this.pushService, appLock, ) } } private getNativeInterface(name: T): NativeInterfaces[T] { if (!this.nativeInterfaces) { throw new ProgrammingError(`Tried to use ${name} in web`) } return this.nativeInterfaces[name] } public getMailImporter(): MailImporter { if (this.mailImporter == null) { throw new ProgrammingError(`Tried to use mail importer in web or mobile`) } return this.mailImporter } private readonly _workerDeferred: DeferredObject private _entropyCollector!: EntropyCollector private _deferredInitialized: DeferredObject = defer() get initialized(): Promise { return this._deferredInitialized.promise } constructor() { this._workerDeferred = defer() } async init(): Promise { // Split init in two separate parts: creating modules and causing side effects. // We would like to do both on normal init but on HMR we just want to replace modules without a new worker. If we create a new // worker we end up losing state on the worker side (including our session). this.worker = bootstrapWorker(this) await this._createInstances() this._entropyCollector = new EntropyCollector(this.entropyFacade, await this.scheduler(), window) this._entropyCollector.start() this._deferredInitialized.resolve() } async _createInstances() { const { loginFacade, customerFacade, giftCardFacade, groupManagementFacade, identityKeyCreator, configFacade, calendarFacade, mailFacade, shareFacade, counterFacade, indexerFacade, searchFacade, bookingFacade, mailAddressFacade, keyVerificationFacade, publicEncryptionKeyProvider, publicIdentityKeyProvider, blobFacade, userManagementFacade, recoverCodeFacade, restInterface, serviceExecutor, cryptoFacade, cacheStorage, random, eventBus, entropyFacade, workerFacade, sqlCipherFacade, contactFacade, bulkMailLoader, mailExportFacade, contactSearchFacade, autosaveFacade, spamClassifier, } = this.worker.getWorkerInterface() as WorkerInterface this.loginFacade = loginFacade this.customerFacade = customerFacade this.giftCardFacade = giftCardFacade this.groupManagementFacade = groupManagementFacade this.identityKeyCreator = identityKeyCreator this.configFacade = configFacade this.calendarFacade = calendarFacade this.mailFacade = mailFacade this.shareFacade = shareFacade this.counterFacade = counterFacade this.indexerFacade = indexerFacade this.searchFacade = searchFacade this.contactSearchFacade = contactSearchFacade this.bookingFacade = bookingFacade this.mailAddressFacade = mailAddressFacade this.keyVerificationFacade = keyVerificationFacade this.publicEncryptionKeyProvider = publicEncryptionKeyProvider this.publicIdentityKeyProvider = publicIdentityKeyProvider this.blobFacade = blobFacade this.userManagementFacade = userManagementFacade this.recoverCodeFacade = recoverCodeFacade this.contactFacade = contactFacade this.serviceExecutor = serviceExecutor this.sqlCipherFacade = sqlCipherFacade this.logins = new LoginController( this.loginFacade, this.customerFacade, async () => this.loginListener, () => this.worker.reset(), ) // Should be called elsewhere later e.g. in CommonLocator this.logins.init() this.eventController = new EventController(mailLocator.logins) this.progressTracker = new ProgressTracker() this.syncTracker = new SyncTracker() this.search = new SearchModel(this.searchFacade, () => this.calendarEventsRepository()) this.entityClient = new EntityClient(restInterface, this.typeModelResolver()) this.cryptoFacade = cryptoFacade this.cacheStorage = cacheStorage this.entropyFacade = entropyFacade this.workerFacade = workerFacade this.bulkMailLoader = bulkMailLoader this.mailExportFacade = mailExportFacade this.connectivityModel = new WebsocketConnectivityModel(eventBus) this.mailboxModel = new MailboxModel(this.eventController, this.entityClient, this.logins) this.mailModel = new MailModel( notifications, this.mailboxModel, this.eventController, this.entityClient, this.logins, this.mailFacade, this.connectivityModel, this.spamClassificationHandler, this.inboxRuleHandler, ) this.operationProgressTracker = new OperationProgressTracker() this.infoMessageHandler = new InfoMessageHandler((state: SearchIndexStateInfo) => { mailLocator.search.indexState(state) }) this.autosaveFacade = autosaveFacade this.usageTestModel = new UsageTestModel( { [StorageBehavior.Persist]: deviceConfig, [StorageBehavior.Ephemeral]: new EphemeralUsageTestStorage(), }, { now(): number { return Date.now() }, timeZone(): string { throw new Error("Not implemented by this provider") }, }, this.serviceExecutor, this.entityClient, this.logins, this.eventController, () => this.usageTestController, this.typeModelResolver(), ) this.usageTestController = new UsageTestController(this.usageTestModel) this.Const = Const this.whitelabelThemeGenerator = new WhitelabelThemeGenerator() if (!isBrowser()) { const { WebDesktopFacade } = await import("../common/native/main/WebDesktopFacade") const { WebMobileFacade } = await import("../common/native/main/WebMobileFacade.js") const { WebCommonNativeFacade } = await import("../common/native/main/WebCommonNativeFacade.js") const { WebInterWindowEventFacade } = await import("../common/native/main/WebInterWindowEventFacade.js") const { WebAuthnFacadeSendDispatcher } = await import("../common/native/common/generatedipc/WebAuthnFacadeSendDispatcher.js") const { OpenMailboxHandler } = await import("./native/main/OpenMailboxHandler.js") const { createNativeInterfaces, createDesktopInterfaces } = await import("../common/native/main/NativeInterfaceFactory.js") const openMailboxHandler = new OpenMailboxHandler(this.logins, this.mailModel, this.mailboxModel) const { OpenCalendarHandler } = await import("../common/native/main/OpenCalendarHandler.js") const openCalendarHandler = new OpenCalendarHandler(this.logins, async (mode: CalendarOperation, date: Date) => { const mailboxDetail = await this.mailboxModel.getUserMailboxDetails() const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetail.mailboxGroupRoot) return await this.calendarEventModel(mode, getEventWithDefaultTimes(setNextHalfHour(new Date(date))), mailboxDetail, mailboxProperties, null) }) const { OpenSettingsHandler } = await import("../common/native/main/OpenSettingsHandler.js") const openSettingsHandler = new OpenSettingsHandler(this.logins) this.webMobileFacade = new WebMobileFacade(this.connectivityModel, MAIL_PREFIX) this.spamClassifier = spamClassifier this.nativeInterfaces = createNativeInterfaces( this.webMobileFacade, new WebDesktopFacade(this.logins, async () => this.native), new WebInterWindowEventFacade(this.logins, windowFacade, deviceConfig), new WebCommonNativeFacade( this.logins, this.mailboxModel, this.usageTestController, async () => this.fileApp, async () => this.pushService, this.handleFileImport.bind(this), (userId, address, requestedPath) => openMailboxHandler.openMailbox(userId, address, requestedPath), (userId, action, date, eventId) => openCalendarHandler.openCalendar(userId, action, date, eventId), AppType.Integrated, (path) => openSettingsHandler.openSettings(path), ), cryptoFacade, calendarFacade, this.entityClient, this.logins, AppType.Integrated, ) this.credentialsProvider = await this.createCredentialsProvider() if (isElectronClient()) { const desktopInterfaces = createDesktopInterfaces(this.native) this.searchTextFacade = desktopInterfaces.searchTextFacade this.interWindowEventSender = desktopInterfaces.interWindowEventSender this.webAuthn = new WebauthnClient(new WebAuthnFacadeSendDispatcher(this.native), this.domainConfigProvider(), isApp()) if (isDesktop()) { this.desktopSettingsFacade = desktopInterfaces.desktopSettingsFacade this.desktopSystemFacade = desktopInterfaces.desktopSystemFacade this.mailImporter = new MailImporter( this.domainConfigProvider(), this.logins, this.mailboxModel, this.entityClient, this.eventController, this.credentialsProvider, desktopInterfaces.nativeMailImportFacade, openSettingsHandler, ) this.exportFacade = desktopInterfaces.exportFacade } } else if (isAndroidApp() || isIOSApp()) { const { SystemPermissionHandler } = await import("../common/native/main/SystemPermissionHandler.js") this.systemPermissionHandler = new SystemPermissionHandler(this.systemFacade) this.webAuthn = new WebauthnClient(new WebAuthnFacadeSendDispatcher(this.native), this.domainConfigProvider(), isApp()) this.systemFacade.storeServerRemoteOrigin(assertNotNull(env.staticUrl)).catch((e) => console.log("Failed to store remote URL: ", e)) } } else { this.credentialsProvider = await this.createCredentialsProvider() } if (this.webAuthn == null) { this.webAuthn = new WebauthnClient( new BrowserWebauthn(navigator.credentials, this.domainConfigProvider().getCurrentDomainConfig()), this.domainConfigProvider(), isApp(), ) } this.secondFactorHandler = new SecondFactorHandler( this.eventController, this.entityClient, this.webAuthn, this.loginFacade, this.domainConfigProvider(), ) this.loginListener = new PageContextLoginListener(this.secondFactorHandler, this.credentialsProvider) this.random = random this.newsModel = new NewsModel(this.serviceExecutor, deviceConfig, async (name: string) => { switch (name) { case "usageOptIn": { const { UsageOptInNews } = await import("../common/misc/news/items/UsageOptInNews.js") return new UsageOptInNews(this.newsModel, this.usageTestModel) } case "recoveryCode": { const { RecoveryCodeNews } = await import("../common/misc/news/items/RecoveryCodeNews.js") return new RecoveryCodeNews(this.newsModel, this.logins.getUserController(), this.recoverCodeFacade) } case "pinBiometrics": { const { PinBiometricsNews } = await import("../common/misc/news/items/PinBiometricsNews.js") return new PinBiometricsNews(this.newsModel, this.credentialsProvider, this.logins.getUserController().userId) } case "referralLink": { const { ReferralLinkNews } = await import("../common/misc/news/items/ReferralLinkNews.js") const dateProvider = await this.noZoneDateProvider() return new ReferralLinkNews(this.newsModel, dateProvider, this.logins.getUserController()) } case "richNotifications": { const { RichNotificationsNews } = await import("../common/misc/news/items/RichNotificationsNews.js") return new RichNotificationsNews(this.newsModel, isApp() || isDesktop() ? this.pushService : null) } case "colorCustomizationUpdate": { const { UpdateColorCustomizationNews } = await import("../common/misc/news/items/UpdateColorCustomizationNews.js") return new UpdateColorCustomizationNews(this.newsModel, this.logins.getUserController()) } default: console.log(`No implementation for news named '${name}'`) return null } }) this.fileController = this.nativeInterfaces == null ? new FileControllerBrowser(blobFacade, guiDownload) : new FileControllerNative(blobFacade, guiDownload, this.nativeInterfaces.fileApp) const { ContactModel } = await import("../common/contactsFunctionality/ContactModel.js") this.contactModel = new ContactModel(this.entityClient, this.logins, this.eventController, this.contactSearchFacade) this.minimizedMailModel = new MinimizedMailEditorViewModel() // THEME // We need it because we want to run tests in node and real HTMLSanitizer does not work there. const sanitizerStub: Partial = { sanitizeHTML: () => { return { html: "", blockedExternalContent: 0, inlineImageCids: [], links: [], } }, sanitizeSVG(svg, configExtra?) { throw new Error("stub!") }, sanitizeFragment(html, configExtra?) { throw new Error("stub!") }, } const selectedThemeFacade = isApp() || isDesktop() ? new NativeThemeFacade(new LazyLoaded(async () => mailLocator.themeFacade)) : new WebThemeFacade(deviceConfig) const lazySanitizer = isTest() ? () => Promise.resolve(sanitizerStub as HtmlSanitizer) : () => import("../common/misc/HtmlSanitizer").then(({ getHtmlSanitizer }) => getHtmlSanitizer()) this.themeController = new ThemeController(theme, selectedThemeFacade, lazySanitizer, AppType.Mail, this.whitelabelThemeGenerator) // For native targets WebCommonNativeFacade notifies themeController because Android and Desktop do not seem to work reliably via media queries if (selectedThemeFacade instanceof WebThemeFacade) { selectedThemeFacade.addDarkListener(() => mailLocator.themeController.reloadTheme()) } } readonly calendarModel: () => Promise = lazyMemoized(async () => { const { DefaultDateProvider } = await import("../common/calendar/date/CalendarUtils") const { CalendarModel } = await import("../calendar-app/calendar/model/CalendarModel") const timeZone = new DefaultDateProvider().timeZone() return new CalendarModel( notifications, this.alarmScheduler, this.eventController, this.serviceExecutor, this.logins, this.progressTracker, this.entityClient, this.mailboxModel, this.calendarFacade, this.fileController, timeZone, !isBrowser() ? this.externalCalendarFacade : null, deviceConfig, !isBrowser() ? this.pushService : null, this.syncTracker, noOp, lang, ) }) readonly calendarInviteHandler: () => Promise = lazyMemoized(async () => { const { CalendarInviteHandler } = await import("../calendar-app/calendar/view/CalendarInvites.js") const { calendarNotificationSender } = await import("../calendar-app/calendar/view/CalendarNotificationSender.js") return new CalendarInviteHandler(this.mailboxModel, await this.calendarModel(), this.logins, calendarNotificationSender, (...arg) => this.sendMailModel(...arg), ) }) private async handleFileImport(filesUris: ReadonlyArray) { const files = await this.fileApp.getFilesMetaData(filesUris) const areAllFilesVCard = files.every((file) => file.mimeType === VCARD_MIME_TYPES.X_VCARD || file.mimeType === VCARD_MIME_TYPES.VCARD) const areAllFilesICS = files.every((file) => file.mimeType === CALENDAR_MIME_TYPE) const areAllFilesMail = files.every((file) => file.mimeType === MAIL_MIME_TYPES.EML || file.mimeType === MAIL_MIME_TYPES.MBOX) if (areAllFilesVCard) { const importer = await this.contactImporter() const { parseContacts } = await import("../mail-app/contacts/ContactImporter.js") // For now, we just handle .vcf files, so we don't need to care about the file type const contacts = await parseContacts(files, this.fileApp) const vCardData = contacts.join("\n") const contactListId = assertNotNull(await this.contactModel.getContactListId()) await importer.importContactsFromFile(vCardData, contactListId) } else if (areAllFilesICS) { const calendarModel = await this.calendarModel() const groupSettings = this.logins.getUserController().userSettingsGroupRoot.groupSettings const calendarInfos = await calendarModel.getCalendarInfos() const groupColors: Map = groupSettings.reduce((acc, gc) => { acc.set(gc.group, gc.color) return acc }, new Map()) const { calendarSelectionDialog, parseCalendarFile } = await import("../common/calendar/gui/CalendarImporter.js") const { handleCalendarImport } = await import("../common/calendar/gui/CalendarImporterDialog.js") let parsedEvents: ParsedEvent[] = [] for (const fileRef of files) { const dataFile = await this.fileApp.readDataFile(fileRef.location) if (dataFile == null) continue const data = parseCalendarFile(dataFile) parsedEvents.push(...data.contents) } calendarSelectionDialog(Array.from(calendarInfos.values()), this.logins.getUserController(), groupColors, (dialog, selectedCalendar) => { dialog.close() handleCalendarImport(selectedCalendar.groupRoot, selectedCalendar, parsedEvents) }) } } private alarmScheduler: () => Promise = lazyMemoized(async () => { const { AlarmScheduler } = await import("../common/calendar/date/AlarmScheduler") const { DefaultDateProvider } = await import("../common/calendar/date/CalendarUtils") const dateProvider = new DefaultDateProvider() return new AlarmScheduler(dateProvider, await this.scheduler()) }) private async scheduler(): Promise { const dateProvider = await this.noZoneDateProvider() return new SchedulerImpl(dateProvider, window, window) } async calendarEventPreviewModel( selectedEvent: CalendarEvent, calendars: ReadonlyMap, highlightedTokens: readonly SearchToken[], ): Promise { const { findAttendeeInAddresses } = await import("../common/api/common/utils/CommonCalendarUtils.js") const { getEventType } = await import("../calendar-app/calendar/gui/CalendarGuiUtils.js") const { CalendarEventPreviewViewModel } = await import("../calendar-app/calendar/gui/eventpopup/CalendarEventPreviewViewModel.js") const mailboxDetails = await this.mailboxModel.getUserMailboxDetails() const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot) const userController = this.logins.getUserController() const customer = await userController.loadCustomer() const ownMailAddresses = getEnabledMailAddressesWithUser(mailboxDetails, userController.userGroupInfo) const ownAttendee: CalendarEventAttendee | null = findAttendeeInAddresses(selectedEvent.attendees, ownMailAddresses) const eventType = getEventType(selectedEvent, calendars, ownMailAddresses, userController) const hasBusinessFeature = isCustomizationEnabledForCustomer(customer, FeatureType.BusinessFeatureEnabled) || (await userController.isNewPaidPlan()) const lazyIndexEntry = async () => (selectedEvent.uid != null ? this.calendarFacade.getEventsByUid(selectedEvent.uid) : null) const popupModel = new CalendarEventPreviewViewModel( selectedEvent, await this.calendarModel(), eventType, hasBusinessFeature, ownAttendee, lazyIndexEntry, async (mode: CalendarOperation, event: CalendarEvent) => this.calendarEventModel(mode, event, mailboxDetails, mailboxProperties, null), highlightedTokens, ) // If we have a preview model we want to display the description // so makes sense to already sanitize it after building the event await popupModel.sanitizeDescription() return popupModel } async calendarContactPreviewModel(event: CalendarEvent, contact: Contact, canEdit: boolean): Promise { const { CalendarContactPreviewViewModel } = await import("../calendar-app/calendar/gui/eventpopup/CalendarContactPreviewViewModel.js") return new CalendarContactPreviewViewModel(event, contact, canEdit) } readonly nativeContactsSyncManager: () => NativeContactsSyncManager = lazyMemoized(() => { assert(isApp(), "isApp") return new NativeContactsSyncManager(this.logins, this.mobileContactsFacade, this.entityClient, this.eventController, this.contactModel, deviceConfig) }) postLoginActions: () => Promise = lazyMemoized(async () => { const { PostLoginActions } = await import("../common/login/PostLoginActions") return new PostLoginActions( this.credentialsProvider, this.secondFactorHandler, this.connectivityModel, this.logins, await this.noZoneDateProvider(), this.entityClient, this.userManagementFacade, this.customerFacade, this.themeController, this.syncTracker, () => this.showSetupWizard(), () => this.updateClients(), this.loginFacade, ) }) showSetupWizard = async () => { if (isApp()) { const { showSetupWizard } = await import("../common/native/main/wizard/SetupWizard.js") return showSetupWizard( this.systemPermissionHandler, this.webMobileFacade, await this.contactImporter(), this.systemFacade, this.credentialsProvider, await this.nativeContactsSyncManager(), deviceConfig, true, ) } } async updateClients(): Promise { if (isDesktop()) { await this.desktopSettingsFacade.manualUpdate() } else if (isApp()) { if (isAndroidApp()) { this.nativeInterfaces?.mobileSystemFacade.openLink("market://details?id=de.tutao.tutanota") } else if (isIOSApp()) { this.nativeInterfaces?.mobileSystemFacade.openLink("itms-apps://itunes.apple.com/app/id922429609") } } else { // web version const registration = await navigator.serviceWorker?.getRegistration() if (registration?.waiting) { registration.waiting.postMessage("update") } else { windowFacade.reload({}) } } } readonly credentialFormatMigrator: () => Promise = lazyMemoized(async () => { const { CredentialFormatMigrator } = await import("../common/misc/credentials/CredentialFormatMigrator.js") if (isDesktop()) { return new CredentialFormatMigrator(deviceConfig, this.nativeCredentialsFacade, null) } else if (isApp()) { return new CredentialFormatMigrator(deviceConfig, this.nativeCredentialsFacade, this.systemFacade) } else { return new CredentialFormatMigrator(deviceConfig, null, null) } }) async addNotificationEmailDialog(): Promise { const { AddNotificationEmailDialog } = await import("../mail-app/settings/AddNotificationEmailDialog.js") return new AddNotificationEmailDialog(this.logins, this.entityClient) } readonly mailExportController: () => Promise = lazyMemoized(async () => { const { getHtmlSanitizer } = await import("../common/misc/HtmlSanitizer") const { MailExportController } = await import("./native/main/MailExportController.js") return new MailExportController(this.mailExportFacade, getHtmlSanitizer(), this.exportFacade, this.logins, this.mailboxModel, await this.scheduler()) }) async offlineStorageSettingsModel(): Promise { if (isOfflineStorageAvailable()) { return new OfflineStorageSettingsModel(this.logins.getUserController(), deviceConfig) } else { return null } } readonly undoModel: lazyAsync = lazyMemoized(async () => { const { UndoModel } = await import("./UndoModel.js") return new UndoModel() }) /** * Factory method for credentials provider that will return an instance injected with the implementations appropriate for the platform. */ private async createCredentialsProvider(): Promise { const { CredentialsProvider } = await import("../common/misc/credentials/CredentialsProvider.js") if (isDesktop() || isApp()) { return new CredentialsProvider(this.nativeCredentialsFacade, this.sqlCipherFacade, isDesktop() ? this.interWindowEventSender : null) } else { const { WebCredentialsFacade } = await import("../common/misc/credentials/WebCredentialsFacade.js") return new CredentialsProvider(new WebCredentialsFacade(deviceConfig), null, null) } } } export type IMailLocator = Readonly export const mailLocator: IMailLocator = new MailLocator() if (typeof window !== "undefined") { window.tutao.locator = mailLocator }