tutanota/src/mail-app/mailLocator.ts
map 5293be6a4a
Implement spam training data sync and add TutanotaModelV98
We sync the spam training data encrypted through our server to make
sure that all clients for a specific user behave the same when
classifying mails. Additionally, this enables the spam classification
in the webApp. We compress the training data vectors
(see clientSpamTrainingDatum) before uploading to our server using
SparseVectorCompressor.ts. When a user has the ClientSpamClassification
enabled, the spam training data sync will happen for every mail
received.

ClientSpamTrainingDatum are not stored in the CacheStorage.
No entityEvents are emitted for this type.
However, we retrieve creations and updates for ClientSpamTrainingData
through the modifiedClientSpamTrainingDataIndex.

We calculate a threshold per classifier based on the dataset ham to spam
ratio, we also subsample our training data to cap the ham to spam ratio
within a certain limit.

Co-authored-by: jomapp <17314077+jomapp@users.noreply.github.com>
Co-authored-by: das <das@tutao.de>
Co-authored-by: abp <abp@tutao.de>
Co-authored-by: Kinan <104761667+kibibytium@users.noreply.github.com>
Co-authored-by: sug <sug@tutao.de>
Co-authored-by: nif <nif@tutao.de>
Co-authored-by: map <mpfau@users.noreply.github.com>
2025-11-18 13:56:19 +01:00

1301 lines
55 KiB
TypeScript

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 "./mail/model/SpamClassificationHandler"
import { SpamClassifier } from "./workerUtils/spamClassification/SpamClassifier"
import { ProcessInboxHandler } from "./mail/model/ProcessInboxHandler"
import type { QuickActionsModel } from "../common/misc/quickactions/QuickActionsModel"
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<string, any>
bulkMailLoader!: BulkMailLoader
mailExportFacade!: MailExportFacade
syncTracker!: SyncTracker
spamClassifier!: SpamClassifier
whitelabelThemeGenerator!: WhitelabelThemeGenerator
autosaveFacade!: AutosaveFacade
private nativeInterfaces: NativeInterfaces | null = null
private mailImporter: MailImporter | null = null
private entropyFacade!: EntropyFacade
private sqlCipherFacade!: SqlCipherFacade
readonly typeModelResolver: lazy<ClientTypeModelResolver> = lazyMemoized(() => {
return ClientModelInfo.getInstance()
})
readonly recipientsModel: lazyAsync<RecipientsModel> = 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<NoZoneDateProvider> {
return new NoZoneDateProvider()
}
async sendMailModel(mailboxDetails: MailboxDetail, mailboxProperties: MailboxProperties): Promise<SendMailModel> {
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<AppHeaderAttrs> {
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.processInboxHandler(),
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.spamClassifier)
})
readonly processInboxHandler = lazyMemoized(() => {
return new ProcessInboxHandler(this.logins, this.mailFacade, this.spamClassificationHandler, this.inboxRuleHandler)
})
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<Router> = lazyMemoized(() => new ThrottledRouter())
readonly scopedSearchRouter: lazyAsync<SearchRouter> = lazyMemoized(async () => {
const { SearchRouter } = await import("../common/search/view/SearchRouter.js")
return new SearchRouter(new ScopedRouter(this.throttledRouter(), "/search"))
})
readonly unscopedSearchRouter: lazyAsync<SearchRouter> = 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 quickActionsModel: lazyAsync<QuickActionsModel> = lazyMemoized(async () => {
const { QuickActionsModel } = await import("../common/misc/quickactions/QuickActionsModel.js")
return new QuickActionsModel()
})
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<Promise<GroupSettingsModel>> = lazyMemoized(async () => {
const { GroupSettingsModel } = await import("../common/sharing/model/GroupSettingsModel.js")
return new GroupSettingsModel(this.entityClient, this.logins)
})
async receivedGroupInvitationsModel<TypeOfGroup extends ShareableGroupType>(groupType: TypeOfGroup): Promise<ReceivedGroupInvitationsModel<TypeOfGroup>> {
const { ReceivedGroupInvitationsModel } = await import("../common/sharing/model/ReceivedGroupInvitationsModel.js")
return new ReceivedGroupInvitationsModel<TypeOfGroup>(groupType, this.eventController, this.entityClient, this.logins)
}
readonly calendarViewModel = lazyMemoized<Promise<CalendarViewModel>>(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<CalendarEventsRepository> = 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<CalendarEvent>,
mailboxDetail: MailboxDetail,
mailboxProperties: MailboxProperties,
responseTo: Mail | null,
): Promise<CalendarEventModel | null> {
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 = <T>(p: Promise<T>) => 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<RecipientsSearchModel> {
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<ContactSuggestionProvider> {
if (isApp()) {
const { MobileContactSuggestionProvider } = await import("../common/native/main/MobileContactSuggestionProvider.js")
return new MobileContactSuggestionProvider(this.mobileContactsFacade)
} else {
return {
async getContactSuggestions(_query: string): Promise<readonly ContactSuggestion[]> {
return []
},
}
}
}
readonly conversationViewModelFactory: lazyAsync<ConversationViewModelFactory> = 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<ConversationViewModel> {
const factory = await this.conversationViewModelFactory()
return factory(options)
}
contactImporter = async (): Promise<ContactImporter> => {
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<MailAddressTableModel> {
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<MailAddressTableModel> {
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<MailAddressNameChanger> {
const { OwnMailAddressNameChanger } = await import("../common/settings/mailaddress/OwnMailAddressNameChanger.js")
return new OwnMailAddressNameChanger(this.mailboxModel, this.entityClient)
}
async adminNameChanger(mailGroupId: Id, userId: Id): Promise<MailAddressNameChanger> {
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<CredentialRemovalHandler> {
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<lazy<LoginViewModel>> {
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<T extends keyof NativeInterfaces>(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<WorkerClient>
private _entropyCollector!: EntropyCollector
private _deferredInitialized: DeferredObject<void> = defer()
get initialized(): Promise<void> {
return this._deferredInitialized.promise
}
constructor() {
this._workerDeferred = defer()
}
async init(): Promise<void> {
// 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.processInboxHandler,
)
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()
this.spamClassifier = spamClassifier
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.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<HtmlSanitizer> = {
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<ThemeFacade>(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<CalendarModel> = 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<CalendarInviteHandler> = 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<string>) {
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<Id, string> = 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<AlarmScheduler> = 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<SchedulerImpl> {
const dateProvider = await this.noZoneDateProvider()
return new SchedulerImpl(dateProvider, window, window)
}
async calendarEventPreviewModel(
selectedEvent: CalendarEvent,
calendars: ReadonlyMap<string, CalendarInfo>,
highlightedTokens: readonly SearchToken[],
): Promise<CalendarEventPreviewViewModel> {
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<CalendarContactPreviewViewModel> {
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<PostLoginActions> = 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<void> {
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<CredentialFormatMigrator> = 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<AddNotificationEmailDialog> {
const { AddNotificationEmailDialog } = await import("../mail-app/settings/AddNotificationEmailDialog.js")
return new AddNotificationEmailDialog(this.logins, this.entityClient)
}
readonly mailExportController: () => Promise<MailExportController> = 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<OfflineStorageSettingsModel | null> {
if (isOfflineStorageAvailable()) {
return new OfflineStorageSettingsModel(this.logins.getUserController(), deviceConfig)
} else {
return null
}
}
readonly undoModel: lazyAsync<UndoModel> = 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<CredentialsProvider> {
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<MailLocator>
export const mailLocator: IMailLocator = new MailLocator()
if (typeof window !== "undefined") {
window.tutao.locator = mailLocator
}