diff --git a/src/e2e/index.spec.ts b/src/e2e/index.spec.ts index 624f4fe4d..ff4226cae 100644 --- a/src/e2e/index.spec.ts +++ b/src/e2e/index.spec.ts @@ -40,10 +40,6 @@ test.serial("general actions: app start, master password setup, add accounts, lo type: "protonmail", entryUrlValue: `${ACCOUNTS_CONFIG_ENTRY_URL_LOCAL_PREFIX}https://protonirockerxow.onion`, }); - await workflow.addAccount({ - type: "tutanota", - entryUrlValue: `${ACCOUNTS_CONFIG_ENTRY_URL_LOCAL_PREFIX}https://mail.tutanota.com`, - }); await workflow.logout(); // login with password saving diff --git a/src/e2e/unread.spec.disabled.ts b/src/e2e/unread.spec.disabled.ts index 09a55ec1e..3b27c8a29 100644 --- a/src/e2e/unread.spec.disabled.ts +++ b/src/e2e/unread.spec.disabled.ts @@ -5,19 +5,14 @@ // tslint:disable:await-promise import {AccountTypeAndLoginFieldContainer} from "src/shared/model/container"; -import {CI, accountBadgeCssSelector, initApp, test} from "./workflow"; import {ONE_SECOND_MS} from "src/shared/constants"; +import {accountBadgeCssSelector, initApp, test} from "./workflow"; // protonmail account to login during e2e tests running const RUNTIME_ENV_E2E_PROTONMAIL_LOGIN = `ELECTRON_MAIL_E2E_PROTONMAIL_LOGIN`; const RUNTIME_ENV_E2E_PROTONMAIL_PASSWORD = `ELECTRON_MAIL_E2E_PROTONMAIL_PASSWORD`; const RUNTIME_ENV_E2E_PROTONMAIL_2FA_CODE = `ELECTRON_MAIL_E2E_PROTONMAIL_2FA_CODE`; const RUNTIME_ENV_E2E_PROTONMAIL_UNREAD_MIN = `ELECTRON_MAIL_E2E_PROTONMAIL_UNREAD_MIN`; -// tutanota account to login during e2e tests running -const RUNTIME_ENV_E2E_TUTANOTA_LOGIN = `ELECTRON_MAIL_E2E_TUTANOTA_LOGIN`; -const RUNTIME_ENV_E2E_TUTANOTA_PASSWORD = `ELECTRON_MAIL_E2E_TUTANOTA_PASSWORD`; -const RUNTIME_ENV_E2E_TUTANOTA_2FA_CODE = `ELECTRON_MAIL_E2E_TUTANOTA_2FA_CODE`; -const RUNTIME_ENV_E2E_TUTANOTA_UNREAD_MIN = `ELECTRON_MAIL_E2E_TUTANOTA_UNREAD_MIN`; for (const {type, login, password, twoFactorCode, unread} of ([ { @@ -27,13 +22,6 @@ for (const {type, login, password, twoFactorCode, unread} of ([ twoFactorCode: process.env[RUNTIME_ENV_E2E_PROTONMAIL_2FA_CODE], unread: Number(process.env[RUNTIME_ENV_E2E_PROTONMAIL_UNREAD_MIN]), }, - { - type: "tutanota", - login: process.env[RUNTIME_ENV_E2E_TUTANOTA_LOGIN], - password: process.env[RUNTIME_ENV_E2E_TUTANOTA_PASSWORD], - twoFactorCode: process.env[RUNTIME_ENV_E2E_TUTANOTA_2FA_CODE], - unread: Number(process.env[RUNTIME_ENV_E2E_TUTANOTA_UNREAD_MIN]), - }, ] as Array)) { if (!login || !password || !unread || isNaN(unread)) { continue; @@ -41,7 +29,7 @@ for (const {type, login, password, twoFactorCode, unread} of ([ test.serial(`unread check: ${type}`, async (t) => { const workflow = await initApp(t, {initial: true}); - const pauseMs = ONE_SECOND_MS * (type === "tutanota" ? (CI ? 80 : 40) : 20); + const pauseMs = ONE_SECOND_MS * 20; const unreadBadgeSelector = accountBadgeCssSelector(); const state: { parsedUnreadText?: string } = {}; diff --git a/src/e2e/workflow.ts b/src/e2e/workflow.ts index 59e84cf23..9783e9c12 100644 --- a/src/e2e/workflow.ts +++ b/src/e2e/workflow.ts @@ -279,14 +279,6 @@ function buildWorkflow(t: ExecutionContext) { await client.waitForVisible(selector = `#goToAccountsSettingsLink`); await workflow._click(selector); - // required: type - await client.waitForVisible(selector = `#accountEditFormTypeField .ng-select-container`); - await workflow._mousedown(selector); - - await client.waitForVisible(selector = `[type-option-value="${account.type}"`); - await workflow._click(selector = `[type-option-value="${account.type}"`); - await client.pause(CONF.timeouts.elementTouched); - // required: entryUrl await client.waitForVisible(selector = `#accountEditFormEntryUrlField .ng-select-container`); await workflow._mousedown(selector); @@ -313,7 +305,7 @@ function buildWorkflow(t: ExecutionContext) { // account got added to the settings modal account list await (async () => { - selector = `.modal-body electron-mail-type-symbol ~ .d-inline-block > span[data-login='${login}']`; + selector = `.modal-body .d-inline-block > span[data-login='${login}']`; const timeout = CONF.timeouts.encryption; try { await t.context.app.client.waitForVisible(selector, timeout); diff --git a/src/electron-main/api/endpoints-builders/database/folders-view.ts b/src/electron-main/api/endpoints-builders/database/folders-view.ts index 45c772381..cdbb12419 100644 --- a/src/electron-main/api/endpoints-builders/database/folders-view.ts +++ b/src/electron-main/api/endpoints-builders/database/folders-view.ts @@ -98,10 +98,6 @@ export const FOLDER_UTILS: { })(); function resolveAccountConversationNodes(account: FsDbAccount): ConversationEntry[] { - if (account.metadata.type === "tutanota") { - return Object.values(account.conversationEntries); - } - const buildEntry = ({pk, mailPk}: Pick) => ({ pk, id: pk, diff --git a/src/electron-main/api/index.spec.ts b/src/electron-main/api/index.spec.ts index b2eab4e02..46256c7dd 100644 --- a/src/electron-main/api/index.spec.ts +++ b/src/electron-main/api/index.spec.ts @@ -147,7 +147,7 @@ const tests: Record const {endpoints} = t.context; const {addAccount, removeAccount} = endpoints; const addProtonPayload = buildProtonmailAccountData(); - const addTutanotaPayload = buildTutanotaAccountData(); + const addProtonPayload2 = buildProtonmailAccountData(); const removePayload = {login: addProtonPayload.login}; const removePayload404 = {login: "404 login"}; @@ -160,7 +160,7 @@ const tests: Record } await addAccount(addProtonPayload); - await addAccount(addTutanotaPayload); + await addAccount(addProtonPayload2); const expectedSettings = produce(await t.context.ctx.settingsStore.readExisting(), (draft) => { (draft._rev as number)++; @@ -181,7 +181,7 @@ const tests: Record await addAccount(buildProtonmailAccountData()); await addAccount(buildProtonmailAccountData()); - let settings = await addAccount(buildTutanotaAccountData()); + let settings = await addAccount(buildProtonmailAccountData()); await t.throwsAsync(changeAccountOrder({login: "login.404", index: 0})); await t.throwsAsync(changeAccountOrder({login: settings.accounts[0].login, index: -1})); @@ -729,15 +729,3 @@ function buildProtonmailAccountData(): Readonly> { - return { - type: "tutanota", - login: generateRandomString(), - entryUrl: generateRandomString(), - credentials: { - password: generateRandomString(), - twoFactorCode: generateRandomString(), - }, - }; -} diff --git a/src/electron-main/context.spec.ts b/src/electron-main/context.spec.ts index 651fb83c7..c19ee6dfd 100644 --- a/src/electron-main/context.spec.ts +++ b/src/electron-main/context.spec.ts @@ -4,6 +4,8 @@ import sinon from "sinon"; import test from "ava"; import {Fs} from "fs-json-store"; +import {PACKAGE_NAME, PACKAGE_VERSION} from "src/shared/constants"; + const ctxDbProps = [ "db", "sessionDb", @@ -24,7 +26,11 @@ ctxDbProps.forEach((ctxDbProp) => { () => import("./context"), (mock) => { mock(() => import("electron")).with({ - app: {getPath: sinon.stub().returns(memFsPath)}, + app: { + getPath: sinon.stub().returns(memFsPath), + getName: () => PACKAGE_NAME, + getVersion: () => PACKAGE_VERSION, + }, } as any); mock(() => import("./constants")).callThrough(); }, @@ -75,7 +81,11 @@ ctxDbProps.forEach((ctxDbProp) => { () => import("./context"), (mock) => { mock(() => import("electron")).with({ - app: {getPath: sinon.stub().returns(memFsPath)}, + app: { + getPath: sinon.stub().returns(memFsPath), + getName: () => PACKAGE_NAME, + getVersion: () => PACKAGE_VERSION, + }, } as any); mock(() => import("./constants")).callThrough().with({ INITIAL_STORES: { @@ -105,6 +115,8 @@ ctxDbProps.forEach((ctxDbProp) => { .callsArg(1) .withArgs("ready") .callsArgWith(1, {}, {on: sinon.spy()}), + getName: () => PACKAGE_NAME, + getVersion: () => PACKAGE_VERSION, }, } as any); mock(() => import("src/electron-main/api/endpoints-builders")).callThrough().with({ diff --git a/src/electron-main/context.ts b/src/electron-main/context.ts index 11f4d7a1c..f84a624a0 100644 --- a/src/electron-main/context.ts +++ b/src/electron-main/context.ts @@ -184,7 +184,6 @@ function initLocations( fullTextSearchBrowserWindow: appRelativePath("./electron-preload/database-indexer.js"), webView: { protonmail: formatFileUrl(appRelativePath("./electron-preload/webview/protonmail.js")), - tutanota: formatFileUrl(appRelativePath("./electron-preload/webview/tutanota.js")), }, }, vendorsAppCssLinkHref: (() => { @@ -201,7 +200,6 @@ function initLocations( protocolBundles: [], webClients: { protonmail: [], - tutanota: [], }, }; diff --git a/src/electron-main/database/index.spec.ts b/src/electron-main/database/index.spec.ts index 8ac940324..de5188da6 100644 --- a/src/electron-main/database/index.spec.ts +++ b/src/electron-main/database/index.spec.ts @@ -47,8 +47,8 @@ test.serial(`save to file call should write through the "EncryptionAdapter.proto const db = new databaseModule.Database(options, fileFs); const folderStub = buildFolder(); - db.initAccount({type: "tutanota", login: "login1"}) - .folders[folderStub.pk] = await validateEntity("folders", folderStub, "tutanota"); + db.initAccount({type: "protonmail", login: "login1"}) + .folders[folderStub.pk] = await validateEntity("folders", folderStub, "protonmail"); t.false(encryptionAdapterWriteSpy.called); @@ -86,8 +86,8 @@ test.serial(`save to file call should write through the "SerializationAdapter.wr const db = new databaseModule.Database(options, fileFs); const folderStub = buildFolder(); - db.initAccount({type: "tutanota", login: "login1"}) - .folders[folderStub.pk] = await validateEntity("folders", folderStub, "tutanota"); + db.initAccount({type: "protonmail", login: "login1"}) + .folders[folderStub.pk] = await validateEntity("folders", folderStub, "protonmail"); const dump = JSON.parse(JSON.stringify(db.readonlyDbInstance())); @@ -117,8 +117,8 @@ test.serial(`save to file call should write through the "SerializationAdapter.wr test("several sequence save calls should persist the same data", async (t) => { const db = buildDatabase(); const folderStub = buildFolder(); - db.initAccount({type: "tutanota", login: "login1"}) - .folders[folderStub.pk] = await validateEntity("folders", folderStub, "tutanota"); + db.initAccount({type: "protonmail", login: "login1"}) + .folders[folderStub.pk] = await validateEntity("folders", folderStub, "protonmail"); await db.saveToFile(); await db.loadFromFile(); @@ -138,7 +138,7 @@ test("getting nonexistent account should initialize its content", async (t) => { const db = buildDatabase(); await db.saveToFile(); const readonlyDbInstanceDump1 = JSON.parse(JSON.stringify(db.readonlyDbInstance())); - db.initAccount({type: "tutanota", login: "login1"}); + db.initAccount({type: "protonmail", login: "login1"}); await db.saveToFile(); const readonlyDbInstanceDump12 = JSON.parse(JSON.stringify(db.readonlyDbInstance())); t.truthy(readonlyDbInstanceDump1); @@ -171,8 +171,8 @@ test("reset", async (t) => { t.deepEqual(JSON.parse(JSON.stringify(db.readonlyDbInstance())), initial); const folderStub = buildFolder(); - db.initAccount({type: "tutanota", login: "login1"}) - .folders[folderStub.pk] = await validateEntity("folders", folderStub, "tutanota"); + db.initAccount({type: "protonmail", login: "login1"}) + .folders[folderStub.pk] = await validateEntity("folders", folderStub, "protonmail"); t.notDeepEqual(JSON.parse(JSON.stringify(db.readonlyDbInstance())), initial); diff --git a/src/electron-main/database/index.ts b/src/electron-main/database/index.ts index c579eb94e..56925e957 100644 --- a/src/electron-main/database/index.ts +++ b/src/electron-main/database/index.ts @@ -18,13 +18,12 @@ export class Database { static buildEmptyDb(): FsDb { return { version: DATABASE_VERSION, - accounts: {tutanota: {}, protonmail: {}}, + accounts: {protonmail: {}}, }; } static buildEmptyAccountMetadata(type: T): FsDbAccount["metadata"] { const metadata: { [key in keyof FsDb["accounts"]]: FsDbAccount["metadata"] } = { - tutanota: {type: "tutanota", groupEntityEventBatchIds: {}}, protonmail: {type: "protonmail", latestEventId: ""}, }; return metadata[type]; diff --git a/src/electron-main/database/util.ts b/src/electron-main/database/util.ts index b562fc618..3381e953c 100644 --- a/src/electron-main/database/util.ts +++ b/src/electron-main/database/util.ts @@ -29,7 +29,6 @@ export const resolveAccountFolders: (account: name: PROTONMAIL_MAILBOX_IDENTIFIERS._.resolveNameByValue(id as any), mailFolderId: id, })), - tutanota: [], }; const result: typeof resolveAccountFolders = (account) => [ @@ -44,7 +43,7 @@ export const resolveAccountFolders: (account: export function patchMetadata( target: FsDbAccount["metadata"], // TODO TS: use patch: Arguments[0]["metadata"], - source: Skip["metadata"], "type"> | Skip["metadata"], "type">, + source: Skip["metadata"], "type">, sourceType: "dbPatch" | "loadDatabase", ): boolean { const logPrefix = `patchMetadata() ${sourceType}`; diff --git a/src/electron-main/web-request.ts b/src/electron-main/web-request.ts index 9dd81db82..5bb92908a 100644 --- a/src/electron-main/web-request.ts +++ b/src/electron-main/web-request.ts @@ -43,13 +43,11 @@ export function initWebRequestListeners(ctx: Context, session: Session) { const resolveProxy: (details: RequestDetails) => RequestProxy | null = (() => { const origins: { [k in AccountType]: string[] } = { ...resolveLocalWebClientOrigins("protonmail", ctx.locations), - ...resolveLocalWebClientOrigins("tutanota", ctx.locations), }; return (details: RequestDetails) => { const proxies: { [k in AccountType]: ReturnType } = { protonmail: resolveRequestProxy("protonmail", details, origins), - tutanota: resolveRequestProxy("tutanota", details, origins), }; const [accountType] = Object.entries(proxies) .filter((([, value]) => Boolean(value))) @@ -73,7 +71,7 @@ export function initWebRequestListeners(ctx: Context, session: Session) { if (requestProxy) { const {name} = getHeader(requestHeaders, HEADERS.request.origin) || {name: HEADERS.request.origin}; - requestHeaders[name] = resolveFakeOrigin(requestProxy.accountType, requestDetails); + requestHeaders[name] = resolveFakeOrigin(requestDetails); PROXIES.set(requestDetails.id, requestProxy); } @@ -97,14 +95,8 @@ export function initWebRequestListeners(ctx: Context, session: Session) { ); } -function resolveFakeOrigin(accountType: AccountType, requestDetails: RequestDetails): string { - if (accountType === "tutanota") { - // WARN: tutanota responds to the specific origins only - // it will not work for example with http://localhost:2015 origin, so they go with a whitelisting - return "http://localhost:9000"; - } - - // protonmail doesn't care much, so we generate the origin from request +function resolveFakeOrigin(requestDetails: RequestDetails): string { + // protonmail doesn't care much about "origin" value, so we generate the origin from request return buildOrigin(new URL(requestDetails.url)); } @@ -151,7 +143,7 @@ function resolveRequestProxy( : null; } -// TODO consider doing initial preflight/OPTIONS call to https://mail.protonmail.com / https://mail.tutanota.com +// TODO consider doing initial preflight/OPTIONS call to https://mail.protonmail.com // and then pick all the "Access-Control-*" header names as a template instead of hardcoding the default headers // since over time the server may start giving other headers const responseHeadersPatchHandlers: { @@ -232,28 +224,6 @@ const responseHeadersPatchHandlers: { }, ); - return responseHeaders; - }, - tutanota: ({requestProxy, responseDetails}) => { - const {responseHeaders} = responseDetails; - - commonPatch({requestProxy, responseDetails}); - - patchResponseHeader( - responseHeaders, - { - name: HEADERS.response.accessControlAllowHeaders, - values: [ - ...(requestProxy.headers.accessControlRequestHeaders || { - values: [ - "content-type", - "v", - ], - }).values, - ], - }, - ); - return responseHeaders; }, }; diff --git a/src/electron-preload/electron-exposure/index.ts b/src/electron-preload/electron-exposure/index.ts index 76203850d..8e3902e88 100644 --- a/src/electron-preload/electron-exposure/index.ts +++ b/src/electron-preload/electron-exposure/index.ts @@ -3,7 +3,6 @@ import {IPC_MAIN_API} from "src/shared/api/main"; import {LOGGER} from "./logger"; import {PROTONMAIL_IPC_WEBVIEW_API} from "src/shared/api/webview/protonmail"; import {ROLLING_RATE_LIMITER} from "src/electron-preload/electron-exposure/rolling-rate-limiter"; -import {TUTANOTA_IPC_WEBVIEW_API} from "src/shared/api/webview/tutanota"; import {registerDocumentClickEventListener} from "src/electron-preload/events-handling"; export const ELECTRON_WINDOW: Readonly = Object.freeze({ @@ -11,7 +10,6 @@ export const ELECTRON_WINDOW: Readonly = Object.freeze({ buildIpcMainClient: IPC_MAIN_API.client.bind(IPC_MAIN_API), buildIpcWebViewClient: Object.freeze({ protonmail: PROTONMAIL_IPC_WEBVIEW_API.client, - tutanota: TUTANOTA_IPC_WEBVIEW_API.client, }), registerDocumentClickEventListener, rollingRateLimiter: ROLLING_RATE_LIMITER, diff --git a/src/electron-preload/webview/constants.ts b/src/electron-preload/webview/constants.ts index abf4aa73e..f60746e11 100644 --- a/src/electron-preload/webview/constants.ts +++ b/src/electron-preload/webview/constants.ts @@ -7,6 +7,5 @@ export const NOTIFICATION_LOGGED_IN_POLLING_INTERVAL = ONE_SECOND_MS; export const NOTIFICATION_PAGE_TYPE_POLLING_INTERVAL = ONE_SECOND_MS * 1.5; export const WEBVIEW_LOGGERS: Record> = { - tutanota: buildLoggerBundle("[WEBVIEW:tutanota]"), protonmail: buildLoggerBundle("[WEBVIEW:protonmail]"), }; diff --git a/src/electron-preload/webview/tutanota/api/build-db-patch.ts b/src/electron-preload/webview/tutanota/api/build-db-patch.ts deleted file mode 100644 index 3a30d5c28..000000000 --- a/src/electron-preload/webview/tutanota/api/build-db-patch.ts +++ /dev/null @@ -1,385 +0,0 @@ -import {defer} from "rxjs"; - -import * as Database from "src/electron-preload/webview/tutanota/lib/database"; -import * as DatabaseModel from "src/shared/model/database"; -import * as Rest from "src/electron-preload/webview/tutanota/lib/rest"; -import {DEFAULT_MESSAGES_STORE_PORTION_SIZE} from "src/shared/constants"; -import {DbPatch} from "src/shared/api/common"; -import {FsDbAccount} from "src/shared/model/database"; -import {TutanotaApi, TutanotaScanApi} from "src/shared/api/webview/tutanota"; -import {WEBVIEW_LOGGERS} from "src/electron-preload/webview/constants"; -import {buildDbPatchRetryPipeline, buildEmptyDbPatch, persistDatabasePatch, resolveIpcMainApi} from "src/electron-preload/webview/util"; -import {buildLoggerBundle} from "src/electron-preload/util"; -import {curryFunctionMembers, isDatabaseBootstrapped} from "src/shared/util"; -import {getUserController, isLoggedIn, isUpsertUpdate, preprocessError} from "src/electron-preload/webview/tutanota/lib/util"; -import {resolveProviderApi} from "src/electron-preload/webview/tutanota/lib/provider-api"; - -interface DbPatchBundle { - patch: DbPatch; - metadata: Skip["metadata"], "type">; -} - -type BuildDbPatchMethodReturnType = TutanotaScanApi["ApiImplReturns"]["buildDbPatch"]; - -const _logger = curryFunctionMembers(WEBVIEW_LOGGERS.tutanota, "[api/build-db-patch]"); - -const buildDbPatchEndpoint: Pick = { - buildDbPatch(input) { - const logger = curryFunctionMembers(_logger, "buildDbPatch()", input.zoneName); - - logger.info(); - - const inputMetadata = input.metadata; - const deferFactory: () => Promise = async () => { - logger.info("delayFactory()"); - - const controller = getUserController(); - - if (!controller || !isLoggedIn()) { - throw new Error("tutanota:buildDbPatch(): user is supposed to be logged-in"); - } - - if (!isDatabaseBootstrapped(inputMetadata)) { - await bootstrapDbPatch( - logger, - async (dbPatch) => { - await persistDatabasePatch( - { - ...dbPatch, - type: input.type, - login: input.login, - }, - logger, - ); - }, - ); - - return; - } - - const preFetch = await (async ( - inputGroupEntityEventBatchIds: DbPatchBundle["metadata"]["groupEntityEventBatchIds"], - ) => { - const fetchedEventBatches: Rest.Model.EntityEventBatch[] = []; - const memberships = Rest.Util.filterSyncingMemberships(controller.user); - const {groupEntityEventBatchIds}: DbPatchBundle["metadata"] = {groupEntityEventBatchIds: {}}; - logger.verbose(`start fetching entity event batches of ${memberships.length} memberships`); - for (const {group} of memberships) { - const startId = await Rest.Util.generateStartId(inputGroupEntityEventBatchIds[group]); - const entityEventBatches: Rest.Model.EntityEventBatch[] = []; - await Rest.fetchEntitiesRangeUntilTheEnd( - Rest.Model.EntityEventBatchTypeRef, group, {start: startId, count: 100}, async (fetched) => { - entityEventBatches.push(...fetched); - }, - ); - fetchedEventBatches.push(...entityEventBatches); - if (entityEventBatches.length) { - groupEntityEventBatchIds[group] = Rest.Util.resolveInstanceId(entityEventBatches[entityEventBatches.length - 1]); - } - } - logger.verbose( - `fetched ${fetchedEventBatches.length} entity event batches from ${memberships.length} memberships`, - ); - return { - missedEventBatches: fetchedEventBatches, - metadata: {groupEntityEventBatchIds}, - }; - })(inputMetadata.groupEntityEventBatchIds); - const metadata: DbPatchBundle["metadata"] = preFetch.metadata; - const patch = await buildDbPatch({eventBatches: preFetch.missedEventBatches, parentLogger: logger}); - - await persistDatabasePatch( - { - patch, - metadata, - type: input.type, - login: input.login, - }, - logger, - ); - - return; - }; - - return defer(deferFactory).pipe( - buildDbPatchRetryPipeline(preprocessError, inputMetadata, _logger), - ); - }, -}; - -async function bootstrapDbPatch( - parentLogger: ReturnType, - triggerStoreCallback: (path: DbPatchBundle) => Promise, -): Promise { - const logger = curryFunctionMembers(parentLogger, "bootstrapDbPatch()"); - const api = await resolveProviderApi(); - const controller = getUserController(); - - if (!controller) { - throw new Error("User controller is supposed to be defined"); - } - - // last entity event batches fetching must be happening before entities fetching - const {metadata} = await (async () => { - const {GENERATED_MAX_ID} = api["src/api/common/EntityFunctions"]; - const {groupEntityEventBatchIds}: DbPatchBundle["metadata"] = {groupEntityEventBatchIds: {}}; - const memberships = Rest.Util.filterSyncingMemberships(controller.user); - for (const {group} of memberships) { - const entityEventBatches = await Rest.fetchEntitiesRange( - Rest.Model.EntityEventBatchTypeRef, - group, - {count: 1, reverse: true, start: GENERATED_MAX_ID}, - ); - if (entityEventBatches.length) { - groupEntityEventBatchIds[group] = Rest.Util.resolveInstanceId(entityEventBatches[0]); - } - } - logger.info([ - `fetched ${Object.keys(groupEntityEventBatchIds).length} "groupEntityEventBatchIds" metadata properties`, - `from ${memberships.length} memberships`, - ].join(" ")); - return {metadata: {groupEntityEventBatchIds}}; - })(); - - logger.verbose("start fetching contacts"); - const contacts = await (async () => { - const {group} = controller.user.userGroup; - const contactList = await api["src/api/main/Entity"].loadRoot(Rest.Model.ContactListTypeRef, group); - return await Rest.fetchAllEntities(Rest.Model.ContactTypeRef, contactList.contacts); - })(); - logger.info(`fetched ${contacts.length} contacts`); - - logger.verbose(`start fetching folders`); - const folders = await Rest.Util.fetchMailFoldersWithSubFolders(controller.user); - logger.info(`fetched ${folders.length} folders`); - - logger.verbose(`construct initial database patch`); - const initialPatch = buildEmptyDbPatch(); - initialPatch.folders.upsert = folders.map(Database.buildFolder); - initialPatch.contacts.upsert = contacts.map(Database.buildContact); - - logger.verbose(`trigger initial storing`); - await triggerStoreCallback({ - patch: initialPatch, - metadata: { - // WARN: don't persist the "latestEventId" value yet as this is intermediate storing - groupEntityEventBatchIds: {}, - }, - }); - - const remainingMails: Rest.Model.Mail[] = await (async () => { - const {fetching: {messagesStorePortionSize = DEFAULT_MESSAGES_STORE_PORTION_SIZE}} - = await (await resolveIpcMainApi(logger))("readConfig")(); - const fetchParams = {start: await Rest.Util.generateStartId(), count: 100, reverse: false}; - - let mailsPersistencePortion: Rest.Model.Mail[] = []; - let mailsFetched = 0; - - for (const folder of folders) { - await Rest.fetchEntitiesRangeUntilTheEnd(Rest.Model.MailTypeRef, folder.mails, fetchParams, async (mails) => { - mailsFetched += mails.length; - logger.verbose(`mails fetch progress: ${mailsFetched}`); - - mailsPersistencePortion.push(...mails); - - const flushThePortion = mailsPersistencePortion.length >= messagesStorePortionSize; - - if (!flushThePortion) { - return; - } - - const intermediatePatch = await buildMailsAndConversationEntriesDbPatch(mailsPersistencePortion, logger); - - // TODO define "mailsPersistencePortion" array as a constant and reset it then like "mailsPersistencePortion.length = 0" - mailsPersistencePortion = []; - - logger.verbose([ - `trigger intermediate storing,`, - `mails: ${intermediatePatch.mails.upsert.length},`, - `conversationEntries: ${intermediatePatch.conversationEntries.upsert.length}`, - ].join(" ")); - - await triggerStoreCallback({ - patch: intermediatePatch, - metadata: { - // WARN: don't persist the "latestEventId" value yet as this is intermediate storing - groupEntityEventBatchIds: {}, - }, - }); - }); - } - - logger.info(`fetched ${mailsFetched} messages`); - - return mailsPersistencePortion; - })(); - - const finalPatch = await buildMailsAndConversationEntriesDbPatch(remainingMails, logger); - - logger.verbose([ - `trigger final storing,`, - `mails: ${finalPatch.mails.upsert.length},`, - `conversationEntries: ${finalPatch.conversationEntries.upsert.length}`, - ].join(" ")); - - await triggerStoreCallback({ - patch: finalPatch, - metadata, - }); -} - -async function buildMailsAndConversationEntriesDbPatch( - mails: Rest.Model.Mail[], - logger: ReturnType, -): Promise { - const conversationEntries = await fetchConversationEntries(mails, logger); - const patch = buildEmptyDbPatch(); - - patch.mails.upsert = await Database.buildMails(mails); - patch.conversationEntries.upsert = conversationEntries.map(Database.buildConversationEntry); - - return patch; -} - -async function fetchConversationEntries( - mails: Rest.Model.Mail[], - logger: ReturnType, -): Promise { - const items: Rest.Model.ConversationEntry[] = []; - const conversationEntryListIds = mails.reduce( - (accumulator, mail) => { - accumulator.add(Rest.Util.resolveListId({_id: mail.conversationEntry})); - return accumulator; - }, - new Set(), - ); - - // TODO figure how to load all the entities in a single/few requests having only the array of "listId" values - logger.verbose(`start fetching conversation entries, iterations count: ${conversationEntryListIds.size}`); - for (const listId of conversationEntryListIds.values()) { - const fetched = await Rest.fetchAllEntities(Rest.Model.ConversationEntryTypeRef, listId); - items.push(...fetched); - logger.verbose(`conversation entries fetch progress: ${fetched.length}`); - } - - return items; -} - -async function buildDbPatch( - input: { - eventBatches: Array>; - parentLogger: ReturnType; - }, - nullUpsert: boolean = false, -): Promise { - const logger = curryFunctionMembers(input.parentLogger, "buildDbPatch()"); - const mappingItem = () => ({updatesMappedByInstanceId: new Map(), remove: [], upsertIds: new Map()}); - const mapping: Record<"conversationEntries" | "mails" | "folders" | "contacts", { - updatesMappedByInstanceId: Map; - remove: Array<{ pk: string }>; - upsertIds: Map; // list.id => instance.id[] - }> & { - conversationEntries: { refType: Rest.Model.TypeRef }, - mails: { refType: Rest.Model.TypeRef }, - folders: { refType: Rest.Model.TypeRef }, - contacts: { refType: Rest.Model.TypeRef }, - } = { - conversationEntries: {refType: Rest.Model.ConversationEntryTypeRef, ...mappingItem()}, - mails: {refType: Rest.Model.MailTypeRef, ...mappingItem()}, - folders: {refType: Rest.Model.MailFolderTypeRef, ...mappingItem()}, - contacts: {refType: Rest.Model.ContactTypeRef, ...mappingItem()}, - }; - const mappingKeys = Object.keys(mapping) as Array; - - for (const {events} of input.eventBatches) { - for (const event of events) { - for (const key of mappingKeys) { - const {refType, updatesMappedByInstanceId} = mapping[key]; - if (!Rest.Util.sameRefType(refType, event)) { - continue; - } - updatesMappedByInstanceId.set(event.instanceId, (updatesMappedByInstanceId.get(event.instanceId) || []).concat(event)); - } - } - } - - logger.verbose([ - `resolved unique entities to process history chain:`, - mappingKeys.map((key) => `${key}: ${mapping[key].updatesMappedByInstanceId.size}`).join("; "), - ].join(" ")); - - for (const key of mappingKeys) { - const {updatesMappedByInstanceId, upsertIds, remove} = mapping[key]; - for (const entityUpdates of updatesMappedByInstanceId.values()) { - let upserted = false; - // entity updates sorted in ASC order, so reversing the entity updates list in order to start processing from the newest items - for (const update of entityUpdates.reverse()) { - if (!upserted && isUpsertUpdate(update)) { - upsertIds.set(update.instanceListId, (upsertIds.get(update.instanceListId) || []).concat(update.instanceId)); - upserted = true; - } - if (update.operation === DatabaseModel.OPERATION_TYPE.DELETE) { - remove.push({pk: Database.buildPk([update.instanceListId, update.instanceId])}); - break; - } - } - } - } - - const patch: DbPatch = { - conversationEntries: {remove: mapping.conversationEntries.remove, upsert: []}, - mails: {remove: mapping.mails.remove, upsert: []}, - folders: {remove: mapping.folders.remove, upsert: []}, - contacts: {remove: mapping.contacts.remove, upsert: []}, - }; - - if (!nullUpsert) { - // TODO process 404 error of fetching individual entity - // so we could catch the individual entity fetching error - // 404 error can be ignored as if it occurs because user was moved stuff from here to there while syncing cycle was in progress - // in order to handle the case there will be a need to switch back to the per entity fetch requests - for (const [listId, instanceIds] of mapping.conversationEntries.upsertIds.entries()) { - const entities = await Rest.fetchMultipleEntities(mapping.conversationEntries.refType, listId, instanceIds); - for (const entity of entities) { - patch.conversationEntries.upsert.push(Database.buildConversationEntry(entity)); - } - } - for (const [listId, instanceIds] of mapping.mails.upsertIds.entries()) { - const entities = await Rest.fetchMultipleEntities(mapping.mails.refType, listId, instanceIds); - patch.mails.upsert.push(...await Database.buildMails(entities)); - } - for (const [listId, instanceIds] of mapping.folders.upsertIds.entries()) { - const entities = await Rest.fetchMultipleEntities(mapping.folders.refType, listId, instanceIds); - for (const entity of entities) { - patch.folders.upsert.push(Database.buildFolder(entity)); - } - } - for (const [listId, instanceIds] of mapping.contacts.upsertIds.entries()) { - const entities = await Rest.fetchMultipleEntities(mapping.contacts.refType, listId, instanceIds); - for (const entity of entities) { - patch.contacts.upsert.push(Database.buildContact(entity)); - } - } - } else { - // we only need the data structure to be formed at this point, so no need to perform the actual fetching - for (const key of mappingKeys) { - for (const instanceIds of mapping[key].upsertIds.values()) { - instanceIds.forEach(() => { - (patch[key].upsert as any[]).push(null); - }); - } - } - } - - logger.verbose([ - `upsert/remove:`, - mappingKeys.map((key) => `${key}: ${patch[key].upsert.length}/${patch[key].remove.length}`).join("; "), - ].join(" ")); - - return patch; -} - -export { - buildDbPatchEndpoint, - buildDbPatch, -}; diff --git a/src/electron-preload/webview/tutanota/api/index.ts b/src/electron-preload/webview/tutanota/api/index.ts deleted file mode 100644 index f98042fef..000000000 --- a/src/electron-preload/webview/tutanota/api/index.ts +++ /dev/null @@ -1,296 +0,0 @@ -import {EMPTY, Observable, from, interval, merge} from "rxjs"; -import {authenticator} from "otplib/otplib-browser"; -import {buffer, concatMap, debounceTime, distinctUntilChanged, map, tap} from "rxjs/operators"; -import {pick} from "ramda"; - -import * as Rest from "src/electron-preload/webview/tutanota/lib/rest"; -import * as WebviewConstants from "src/electron-preload/webview/constants"; -import {MAIL_FOLDER_TYPE} from "src/shared/model/database"; -import {ONE_SECOND_MS} from "src/shared/constants"; -import {TUTANOTA_IPC_WEBVIEW_API, TutanotaApi, TutanotaNotificationOutput} from "src/shared/api/webview/tutanota"; -import {buildDbPatch, buildDbPatchEndpoint} from "./build-db-patch"; -import {curryFunctionMembers, isEntityUpdatesPatchNotEmpty} from "src/shared/util"; -import {fillInputValue, getLocationHref, resolveDomElements, resolveIpcMainApi, submitTotpToken} from "src/electron-preload/webview/util"; -import {getUserController, isLoggedIn} from "src/electron-preload/webview/tutanota/lib/util"; -import {resolveProviderApi} from "src/electron-preload/webview/tutanota/lib/provider-api"; - -const _logger = curryFunctionMembers(WebviewConstants.WEBVIEW_LOGGERS.tutanota, "[api/index]"); - -export async function registerApi(): Promise { - return resolveProviderApi() - .then(bootstrapEndpoints) - .then((endpoints) => { - TUTANOTA_IPC_WEBVIEW_API.register(endpoints, {logger: _logger}); - }); -} - -function bootstrapEndpoints(api: Unpacked>): TutanotaApi { - _logger.info("bootstrapEndpoints"); - - const {GENERATED_MAX_ID} = api["src/api/common/EntityFunctions"]; - const login2FaWaitElementsConfig = { - input: () => document.querySelector("#modal input.input") as HTMLInputElement, - button: () => document.querySelector("#modal .dialog-header .justify-end button") as HTMLElement, - }; - const endpoints: TutanotaApi = { - ...buildDbPatchEndpoint, - - async ping() {}, - - async selectAccount({databaseView, zoneName}) { - const logger = curryFunctionMembers(_logger, "selectAccount()", zoneName); - - logger.info("selectAccount()", zoneName); - - await (await resolveIpcMainApi(logger))("selectAccount")({databaseView}); - }, - - async selectMailOnline(input) { - _logger.info("selectMailOnline()", input.zoneName); - - const {tutao} = window; - const mailId = input.mail.id; - const [folderId] = input.mail.mailFolderIds; - - if (!tutao) { - throw new Error(`Failed to resolve "tutao" service`); - } - - tutao.m.route.set(`/mail/${folderId}/${mailId}`); - }, - - async fetchSingleMail(input) { - _logger.info("fetchSingleMail()", input.zoneName); - throw new Error("Not yet supported for Tutanota"); - }, - - async makeRead(input) { - _logger.info("makeRead()", input.zoneName); - throw new Error("Not yet supported for Tutanota"); - }, - - async fillLogin({login, zoneName}) { - const logger = curryFunctionMembers(_logger, "fillLogin()", zoneName); - - logger.info(); - - const cancelEvenHandler = (event: MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - }; - const elements = await resolveDomElements( - { - username: () => document.querySelector("form [type=email]") as HTMLInputElement, - storePasswordCheckbox: () => document.querySelector("form .items-center [type=checkbox]") as HTMLInputElement, - storePasswordCheckboxBlock: () => document.querySelector("form .checkbox.pt.click") as HTMLInputElement, - }, - logger, - ); - logger.verbose(`elements resolved`); - - await fillInputValue(elements.username, login); - elements.username.readOnly = true; - logger.verbose(`input values filled`); - - elements.storePasswordCheckbox.checked = false; - elements.storePasswordCheckbox.disabled = true; - elements.storePasswordCheckboxBlock.removeEventListener("click", cancelEvenHandler); - elements.storePasswordCheckboxBlock.addEventListener("click", cancelEvenHandler, true); - logger.verbose(`"store" checkbox disabled`); - }, - - async login({login, password, zoneName}) { - const logger = curryFunctionMembers(_logger, "login()", zoneName); - - logger.info(); - - await endpoints.fillLogin({login, zoneName}); - logger.verbose(`fillLogin() executed`); - - const elements = await resolveDomElements( - { - password: () => document.querySelector("form [type=password]") as HTMLInputElement, - submit: () => document.querySelector("form button") as HTMLElement, - }, - logger, - ); - logger.verbose(`elements resolved`); - - if (elements.password.value) { - throw new Error(`Password is not supposed to be filled already on "login" stage`); - } - - await fillInputValue(elements.password, password); - logger.verbose(`input values filled`); - - elements.submit.click(); - logger.verbose(`clicked`); - }, - - async login2fa({secret, zoneName}) { - const logger = curryFunctionMembers(_logger, "login2fa()", zoneName); - - logger.info(); - - const elements = await resolveDomElements(login2FaWaitElementsConfig, logger); - logger.verbose(`elements resolved`); - - const spacesLessSecret = secret.replace(/\s/g, ""); - - return await submitTotpToken( - elements.input, - elements.button, - () => authenticator.generate(spacesLessSecret), - logger, - ); - }, - - notification: ({entryUrl, zoneName}) => { - const logger = curryFunctionMembers(_logger, "notification()", zoneName); - - logger.info(); - - type TitleOutput = Required>; - type LoggedInOutput = Required>; - type PageTypeOutput = Required>; - type UnreadOutput = Required>; - type BatchEntityUpdatesCounterOutput = Required>; - - // TODO add "entity event batches" listening notification instead of the polling - // so app reacts to the mails/folders updates instantly - - const observables: [ - Observable, - Observable, - Observable, - Observable, - Observable - ] = [ - new Observable((observer) => { - observer.next({title: `"title" DOM element listening is not enabled`}); - observer.complete(); - }), - - interval(WebviewConstants.NOTIFICATION_LOGGED_IN_POLLING_INTERVAL).pipe( - map(() => isLoggedIn()), - distinctUntilChanged(), - map((loggedIn) => ({loggedIn})), - ), - - // TODO listen for location.href change instead of starting polling interval - interval(WebviewConstants.NOTIFICATION_PAGE_TYPE_POLLING_INTERVAL).pipe( - concatMap(() => from((async () => { - const url = getLocationHref(); - const pageType: PageTypeOutput["pageType"] = {url, type: "unknown"}; - const loginUrlDetected = (url === `${entryUrl}/login` || url.startsWith(`${entryUrl}/login?`)); - - if (loginUrlDetected && !isLoggedIn()) { - let twoFactorElements; - - try { - twoFactorElements = await resolveDomElements(login2FaWaitElementsConfig, logger, {iterationsLimit: 1}); - } catch (e) { - // NOOP - } - - const twoFactorCodeVisible = twoFactorElements - && twoFactorElements.input.offsetParent - && twoFactorElements.button.offsetParent; - - if (twoFactorCodeVisible) { - pageType.type = "login2fa"; - } else { - pageType.type = "login"; - } - } - - return {pageType}; - })())), - distinctUntilChanged(({pageType: prev}, {pageType: curr}) => curr.type === prev.type), - tap((value) => logger.verbose(JSON.stringify(value))), - ), - - // TODO listen for "unread" change instead of starting polling interval - new Observable((subscriber) => { - const notifyUnreadValue = async () => { - const controller = getUserController(); - - if (!controller || !isLoggedIn() || !navigator.onLine) { - return; - } - - const folders = await Rest.Util.fetchMailFoldersWithSubFolders(controller.user); - const inboxFolder = folders.find(({folderType}) => folderType === MAIL_FOLDER_TYPE.INBOX); - - if (!inboxFolder) { - return; - } - - const emails = await Rest.fetchEntitiesRange( - Rest.Model.MailTypeRef, - inboxFolder.mails, - {count: 50, reverse: true, start: GENERATED_MAX_ID}, - ); - const unread = emails.reduce((sum, mail) => sum + Number(mail.unread), 0); - - subscriber.next({unread}); - }; - - setInterval(notifyUnreadValue, ONE_SECOND_MS * 60); - setTimeout(notifyUnreadValue, ONE_SECOND_MS * 15); - }).pipe( - // all the values need to be sent to stream to get proper unread value after disabling database syncing - // distinctUntilChanged(({unread: prev}, {unread: curr}) => curr === prev), - ), - - (() => { - const innerLogger = curryFunctionMembers(logger, `[entity update notification]`); - const notification = {batchEntityUpdatesCounter: 0}; - const notificationReceived$ = new Observable>(( - notificationReceivedSubscriber, - ) => { - const {EventController} = api["src/api/main/EventController"]; - EventController.prototype.notificationReceived = (( - original = EventController.prototype.notificationReceived, - ) => { - const overridden: typeof EventController.prototype.notificationReceived = function( - this: typeof EventController, - entityUpdates, - // tslint:disable-next-line:trailing-comma - ...rest - ) { - entityUpdates - .map((entityUpdate) => pick(["application", "type", "operation"], entityUpdate)) - .forEach((entityUpdate) => innerLogger.debug(JSON.stringify(entityUpdate))); - notificationReceivedSubscriber.next({events: entityUpdates}); - return original.call(this, entityUpdates, ...rest); - }; - return overridden; - })(); - }); - - return notificationReceived$.pipe( - buffer(notificationReceived$.pipe( - debounceTime(ONE_SECOND_MS * 1.5), - )), - concatMap((eventBatches) => from(buildDbPatch({eventBatches, parentLogger: innerLogger}, true))), - concatMap((patch) => { - if (!isEntityUpdatesPatchNotEmpty(patch)) { - return EMPTY; - } - for (const key of (Object.keys(patch) as Array)) { - innerLogger.info(`upsert/remove ${key}: ${patch[key].upsert.length}/${patch[key].remove.length}`); - } - notification.batchEntityUpdatesCounter++; - return [notification]; - }), - ); - })(), - ]; - - return merge(...observables); - }, - }; - - return endpoints; -} diff --git a/src/electron-preload/webview/tutanota/configure-provider-app.ts b/src/electron-preload/webview/tutanota/configure-provider-app.ts deleted file mode 100644 index 75c197da6..000000000 --- a/src/electron-preload/webview/tutanota/configure-provider-app.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {WEBVIEW_LOGGERS} from "src/electron-preload/webview/constants"; -import {curryFunctionMembers} from "src/shared/util"; -import {disableBrowserNotificationFeature, disableBrowserServiceWorkerFeature, isBuiltInWebClient} from "src/electron-preload/webview/util"; -import {initSpellCheckProvider} from "src/electron-preload/spell-check"; -import {registerDocumentClickEventListener, registerDocumentKeyDownEventListener} from "src/electron-preload/events-handling"; - -const logger = curryFunctionMembers(WEBVIEW_LOGGERS.tutanota, `[configure-provider-app]`); - -export function configureProviderApp() { - logger.info(`configureProviderApp()`, JSON.stringify({location: location.href})); - - disableBrowserNotificationFeature(logger); - - if (isBuiltInWebClient()) { - disableBrowserServiceWorkerFeature(logger); - } - - enableEventsProcessing(); - - initSpellCheckProvider(logger); -} - -function enableEventsProcessing() { - registerDocumentKeyDownEventListener(document, logger); - registerDocumentClickEventListener(document, logger); -} diff --git a/src/electron-preload/webview/tutanota/deprecation-warning.ts b/src/electron-preload/webview/tutanota/deprecation-warning.ts deleted file mode 100644 index 7af25c843..000000000 --- a/src/electron-preload/webview/tutanota/deprecation-warning.ts +++ /dev/null @@ -1,55 +0,0 @@ -export function setupDeprecationWarning() { - const observer = new MutationObserver((mutations) => { - for (const mutation of mutations) { - mutation.addedNodes.forEach(processAddedNode); - } - }); - const processAddedNode = (addedNode: Node | Element) => { - if ( - !("id" in addedNode) - || - addedNode.id !== "root" - ) { - return; - } - observer.disconnect(); - mountWarningToDOM(); - }; - - observer.observe( - document, - {childList: true, subtree: true}, - ); -} - -function mountWarningToDOM() { - const block = document.createElement("div"); - - Object.assign( - block.style, - ((cssStyleDeclaration: Partial) => cssStyleDeclaration)({ - backgroundColor: "#f8d7da", - border: "1px solid #721c24", - color: "#721c24", - left: "25%", - padding: ".75em", - position: "fixed", - textAlign: "center", - top: "30px", - width: "50%", - zIndex: "50000", - }), - ); - - block.innerHTML = ` -

- Tutanota support is deprecated by ElectronMail since July 2019 and going to be removed with next release. - See issue #180 for details. -

- - Close - - `; - - document.body.appendChild(block); -} diff --git a/src/electron-preload/webview/tutanota/index.ts b/src/electron-preload/webview/tutanota/index.ts deleted file mode 100644 index 0f17ff823..000000000 --- a/src/electron-preload/webview/tutanota/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {filter, take} from "rxjs/operators"; -import {timer} from "rxjs"; - -import {ONE_SECOND_MS} from "src/shared/constants"; -import {WEBVIEW_LOGGERS} from "src/electron-preload/webview/constants"; -import {configureProviderApp} from "./configure-provider-app"; -import {curryFunctionMembers} from "src/shared/util"; -import {registerApi} from "./api"; -import {setupDeprecationWarning} from "./deprecation-warning"; - -const logger = curryFunctionMembers(WEBVIEW_LOGGERS.tutanota, "[index]"); - -bootstrap(); - -setupDeprecationWarning(); - -function bootstrap() { - configureProviderApp(); - - timer(0, ONE_SECOND_MS).pipe( - filter(() => navigator.onLine), - take(1), - ).subscribe(() => { - registerApi() - .then(() => { - logger.verbose(`api registered, url: ${location.href}`); - }) - .catch(logger.error); - }); -} diff --git a/src/electron-preload/webview/tutanota/lib/database/contact.ts b/src/electron-preload/webview/tutanota/lib/database/contact.ts deleted file mode 100644 index 74ff2d475..000000000 --- a/src/electron-preload/webview/tutanota/lib/database/contact.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {pick} from "ramda"; - -import * as DatabaseModel from "src/shared/model/database"; -import * as Rest from "src/electron-preload/webview/tutanota/lib/rest"; -import {buildBaseEntity} from "."; - -export function buildContact(input: Rest.Model.Contact): DatabaseModel.Contact { - return { - ...buildBaseEntity(input), - ...pick(["comment", "company", "firstName", "lastName", "nickname", "role", "title"], input), - addresses: input.addresses.map(ContactAddress), - birthday: input.birthday ? Birthday(input.birthday) : undefined, - mailAddresses: input.mailAddresses.map(ContactMailAddress), - phoneNumbers: input.phoneNumbers.map(ContactPhoneNumber), - socialIds: input.socialIds.map(ContactSocialId), - }; -} - -function ContactAddress(input: Rest.Model.ContactAddress): DatabaseModel.ContactAddress { - return { - ...buildBaseEntity(input), - type: DatabaseModel.CONTACT_ADDRESS_TYPE._.parseValue(input.type), - customTypeName: input.customTypeName, - address: input.address, - }; -} - -function Birthday(input: Rest.Model.Birthday): DatabaseModel.Birthday { - return { - ...buildBaseEntity(input), - ...pick(["day", "month", "year"], input), - }; -} - -function ContactMailAddress(input: Rest.Model.ContactMailAddress): DatabaseModel.ContactMailAddress { - return { - ...buildBaseEntity(input), - type: DatabaseModel.CONTACT_ADDRESS_TYPE._.parseValue(input.type), - customTypeName: input.customTypeName, - address: input.address, - }; -} - -function ContactSocialId(input: Rest.Model.ContactSocialId): DatabaseModel.ContactSocialId { - return { - ...buildBaseEntity(input), - type: DatabaseModel.CONTACT_SOCIAL_TYPE._.parseValue(input.type), - customTypeName: input.customTypeName, - socialId: input.socialId, - }; -} - -function ContactPhoneNumber(input: Rest.Model.ContactPhoneNumber): DatabaseModel.ContactPhoneNumber { - return { - ...buildBaseEntity(input), - type: DatabaseModel.CONTACT_PHONE_NUMBER_TYPE._.parseValue(input.type), - customTypeName: input.customTypeName, - number: input.number, - }; -} diff --git a/src/electron-preload/webview/tutanota/lib/database/conversation-entry.ts b/src/electron-preload/webview/tutanota/lib/database/conversation-entry.ts deleted file mode 100644 index ea0b3fd7e..000000000 --- a/src/electron-preload/webview/tutanota/lib/database/conversation-entry.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as DatabaseModel from "src/shared/model/database"; -import * as Rest from "src/electron-preload/webview/tutanota/lib/rest"; -import {buildBaseEntity, buildPk} from "."; - -export function buildConversationEntry(input: Rest.Model.ConversationEntry): DatabaseModel.ConversationEntry { - return { - ...buildBaseEntity(input), - conversationType: DatabaseModel.CONVERSATION_TYPE._.parseValue(input.conversationType), - messageId: input.messageId, - mailPk: input.mail ? buildPk(input.mail) : undefined, - previousPk: input.previous ? buildPk(input.previous) : undefined, - }; -} diff --git a/src/electron-preload/webview/tutanota/lib/database/folder.ts b/src/electron-preload/webview/tutanota/lib/database/folder.ts deleted file mode 100644 index 370ffe5d7..000000000 --- a/src/electron-preload/webview/tutanota/lib/database/folder.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as DatabaseModel from "src/shared/model/database"; -import * as Rest from "src/electron-preload/webview/tutanota/lib/rest"; -import {buildBaseEntity} from "."; - -export function buildFolder(input: Rest.Model.MailFolder): DatabaseModel.Folder { - return { - ...buildBaseEntity(input), - folderType: DatabaseModel.MAIL_FOLDER_TYPE._.parseValue(input.folderType), - name: input.name, - mailFolderId: input.mails, - }; -} diff --git a/src/electron-preload/webview/tutanota/lib/database/index.ts b/src/electron-preload/webview/tutanota/lib/database/index.ts deleted file mode 100644 index 4f1f8d685..000000000 --- a/src/electron-preload/webview/tutanota/lib/database/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as DatabaseModel from "src/shared/model/database"; -import * as Rest from "src/electron-preload/webview/tutanota/lib/rest"; - -export {buildContact} from "./contact"; -export {buildConversationEntry} from "./conversation-entry"; -export {buildFolder} from "./folder"; -export {buildMails} from "./mail"; - -export function buildPk(id: ID): DatabaseModel.Entity["pk"] { - if (Array.isArray(id)) { - return JSON.stringify(id); - } - if (typeof id === "string") { - return id; - } - throw new Error(`Invalid "id" type`); -} - -export function buildBaseEntity>(input: T) { - return { - pk: buildPk(input._id), - raw: JSON.stringify(input), - id: Rest.Util.resolveInstanceId(input), - }; -} diff --git a/src/electron-preload/webview/tutanota/lib/database/mail.ts b/src/electron-preload/webview/tutanota/lib/database/mail.ts deleted file mode 100644 index 903cb5539..000000000 --- a/src/electron-preload/webview/tutanota/lib/database/mail.ts +++ /dev/null @@ -1,91 +0,0 @@ -import {equals} from "ramda"; - -import * as DatabaseModel from "src/shared/model/database"; -import * as Rest from "src/electron-preload/webview/tutanota/lib/rest"; -import {buildBaseEntity, buildPk} from "."; -import {mapBy} from "src/shared/util"; -import {resolveInstanceId, resolveListId} from "src/electron-preload/webview/tutanota/lib/util"; - -const directTypeMapping: Readonly, DatabaseModel.Mail["state"]>> = { - [Rest.Model.MAIL_STATE.RECEIVED]: DatabaseModel.MAIL_STATE.RECEIVED, - [Rest.Model.MAIL_STATE.DRAFT]: DatabaseModel.MAIL_STATE.DRAFT, - [Rest.Model.MAIL_STATE.SENT]: DatabaseModel.MAIL_STATE.SENT, - [Rest.Model.MAIL_STATE.SENDING]: DatabaseModel.MAIL_STATE.TUTANOTA_SENDING, -}; - -export async function buildMails(mails: Rest.Model.Mail[]): Promise { - const [bodies, files] = await Promise.all([ - // WARN: don't set huge chunk size for mails body loading - // or server will response with timeout error on "/rest/tutanota/mailbody/" request - await Rest.fetchMultipleEntities(Rest.Model.MailBodyTypeRef, null, mails.map(({body}) => body), 20), - await (async () => { - const attachmentsIds = mails.reduce( - (accumulator: Unpacked["attachments"], mail) => [...accumulator, ...mail.attachments], - [], - ); - const attachmentsMap = mapBy(attachmentsIds, (_id) => resolveListId({_id})); - const attachments: Rest.Model.File[] = []; - - for (const [listId, fileIds] of attachmentsMap.entries()) { - const instanceIds = fileIds.map(((_id) => resolveInstanceId({_id}))); - attachments.push(...await Rest.fetchMultipleEntities(Rest.Model.FileTypeRef, listId, instanceIds)); - } - - return attachments; - })(), - ]); - - return mails.reduce((result: DatabaseModel.Mail[], mail) => { - const body = bodies.find(({_id}) => _id === mail.body); - - if (!body) { - throw new Error(`Failed to resolve mail body by "body._id"=${mail.body}`); - } - - return [ - ...result, - Mail( - mail, - body, - files.filter((file) => mail.attachments.find((attachmentId) => equals(attachmentId, file._id))), - ), - ]; - }, []); -} - -function Mail(input: Rest.Model.Mail, body: Rest.Model.MailBody, files: Rest.Model.File[]): DatabaseModel.Mail { - return { - ...buildBaseEntity(input), - conversationEntryPk: buildPk(input.conversationEntry), - mailFolderIds: [resolveListId(input)], - sentDate: Number(input.sentDate), - subject: input.subject, - body: (body.text || body.compressedText) as string, - sender: Address(input.sender), - toRecipients: input.toRecipients.map(Address), - ccRecipients: input.ccRecipients.map(Address), - bccRecipients: input.bccRecipients.map(Address), - attachments: files.map(File), - unread: Boolean(input.unread), - state: directTypeMapping[input.state], - confidential: input.confidential, - replyType: input.replyType, - }; -} - -function Address(input: Rest.Model.MailAddress): DatabaseModel.MailAddress { - return { - ...buildBaseEntity(input), - name: input.name, - address: input.address, - }; -} - -function File(input: Rest.Model.File): DatabaseModel.File { - return { - ...buildBaseEntity(input), - mimeType: input.mimeType, - name: input.name, - size: Number(input.size), - }; -} diff --git a/src/electron-preload/webview/tutanota/lib/provider-api.ts b/src/electron-preload/webview/tutanota/lib/provider-api.ts deleted file mode 100644 index 20d750b31..000000000 --- a/src/electron-preload/webview/tutanota/lib/provider-api.ts +++ /dev/null @@ -1,114 +0,0 @@ -import * as Rest from "./rest"; -import {StatusCodeError} from "src/shared/model/error"; -import {Timestamp} from "src/shared/model/common"; -import {WEBVIEW_LOGGERS} from "src/electron-preload/webview/constants"; -import {curryFunctionMembers} from "src/shared/util"; - -type ModuleFiles = - | "src/api/common/EntityFunctions" - | "src/api/common/TutanotaConstants" - | "src/api/common/utils/Encoding" - | "src/api/main/Entity" - | "src/api/main/EventController"; - -const _logger = curryFunctionMembers(WEBVIEW_LOGGERS.tutanota, "[lib/provider-api]"); -const domContentLoadedPromise = new Promise((resolve) => { - document.addEventListener("DOMContentLoaded", () => resolve()); -}); - -class EventController { - notificationReceived(...[]: readonly Rest.Model.EntityUpdate[][]): void {} -} - -interface ProviderApi extends Record { - "src/api/common/EntityFunctions": { - GENERATED_MIN_ID: Rest.Model.Id; - GENERATED_MAX_ID: Rest.Model.Id; - resolveTypeReference: >( - typeRef: Rest.Model.TypeRef, - ) => Promise<{ type: "ELEMENT_TYPE" | "LIST_ELEMENT_TYPE" | "DATA_TRANSFER_TYPE" | "AGGREGATED_TYPE"; version: string; }>; - }; - "src/api/common/TutanotaConstants": { - FULL_INDEXED_TIMESTAMP: Timestamp; - }; - "src/api/common/utils/Encoding": { - timestampToGeneratedId: (timestamp: Timestamp) => Rest.Model.Id; - generatedIdToTimestamp: (id: Rest.Model.Id) => Timestamp; - }; - "src/api/main/Entity": { - loadRoot: >( - typeRef: Rest.Model.TypeRef, - groupId: Rest.Model.GroupMembership["group"], - ) => Promise; - load: >( - typeRef: Rest.Model.TypeRef, - id: T["_id"], - ) => Promise; - loadAll: >( - typeRef: Rest.Model.TypeRef, - listId: T["_id"][0], - ) => Promise; - loadMultiple: >( - typeRef: Rest.Model.TypeRef, - listId: T["_id"] extends Rest.Model.IdTuple ? T["_id"][0] : null, - instanceIds: Array, - ) => Promise; - loadRange: >( - typeRef: Rest.Model.TypeRef, - listId: T["_id"][0], - start: Rest.Model.Id, - count: number, - reverse: boolean, - ) => Promise; - }; - "src/api/main/EventController": { - EventController: typeof EventController; - }; -} - -const state: { bundle?: ProviderApi } = {}; - -export async function resolveProviderApi(): Promise { - if (state.bundle) { - return state.bundle; - } - - _logger.info("resolveProviderApi"); - - if (!navigator.onLine) { - throw new StatusCodeError(`"resolveProviderApi" failed due to the offline status`, "NoNetworkConnection"); - } - - // TODO reject with timeout - await domContentLoadedPromise; - - const {SystemJS} = window; - const baseURL = String(SystemJS.getConfig().baseURL).replace(/(.*)\/$/, "$1"); - const bundle: Record = { - "src/api/common/EntityFunctions": null, - "src/api/common/TutanotaConstants": null, - "src/api/common/utils/Encoding": null, - "src/api/main/Entity": null, - "src/api/main/EventController": null, - }; - - for (const key of Object.keys(bundle) as Array) { - bundle[key] = await SystemJS.import(`${baseURL}/${key}.js`); - } - - state.bundle = bundle as ProviderApi; - - // TODO validate types of all the described constants/functions in a declarative way - // so app gets tutanota breaking changes noticed on early stage - if (typeof bundle["src/api/common/EntityFunctions"].GENERATED_MIN_ID !== "string") { - throw new Error(`Invalid "src/api/common/EntityFunctions.GENERATED_MIN_ID" value`); - } - if (typeof bundle["src/api/common/EntityFunctions"].GENERATED_MAX_ID !== "string") { - throw new Error(`Invalid "src/api/common/EntityFunctions.GENERATED_MAX_ID" value`); - } - if (typeof bundle["src/api/common/TutanotaConstants"].FULL_INDEXED_TIMESTAMP !== "number") { - throw new Error(`Invalid "src/api/common/TutanotaConstants.FULL_INDEXED_TIMESTAMP" value`); - } - - return state.bundle; -} diff --git a/src/electron-preload/webview/tutanota/lib/rest/index.ts b/src/electron-preload/webview/tutanota/lib/rest/index.ts deleted file mode 100644 index 415aca088..000000000 --- a/src/electron-preload/webview/tutanota/lib/rest/index.ts +++ /dev/null @@ -1,100 +0,0 @@ -import {splitEvery} from "ramda"; - -import * as Model from "./model"; -import * as Util from "src/electron-preload/webview/tutanota/lib/util"; -import {BaseEntity, Id, IdTuple, RequestParams, TypeRef} from "./model"; -import {WEBVIEW_LOGGERS} from "src/electron-preload/webview/constants"; -import {curryFunctionMembers} from "src/shared/util"; -import {resolveProviderApi} from "src/electron-preload/webview/tutanota/lib/provider-api"; - -const logger = curryFunctionMembers(WEBVIEW_LOGGERS.tutanota, "[lib/rest"); - -export async function fetchEntity>( - typeRef: TypeRef, - id: T["_id"], -): Promise { - logger.debug("fetchEntity()"); - - const {load} = (await resolveProviderApi())["src/api/main/Entity"]; - - return load(typeRef, id); -} - -export async function fetchAllEntities>( - typeRef: TypeRef, - listId: T["_id"][0], -): Promise { - logger.debug("fetchAllEntities()"); - - const {loadAll} = (await resolveProviderApi())["src/api/main/Entity"]; - - return loadAll(typeRef, listId); -} - -export async function fetchMultipleEntities>( - typeRef: TypeRef, - listId: T["_id"] extends IdTuple ? T["_id"][0] : null, - instanceIds: Array, - chunkSize = 100, -): Promise { - logger.debug("fetchMultipleEntities()"); - - const {loadMultiple} = (await resolveProviderApi())["src/api/main/Entity"]; - const instanceIdsChunks = splitEvery(chunkSize, instanceIds); - const result: T[] = []; - - for (const instanceIdsChunk of instanceIdsChunks) { - result.push(...await loadMultiple(typeRef, listId, instanceIdsChunk)); - } - - return result; -} - -export async function fetchEntitiesRange>( - typeRef: TypeRef, - listId: T["_id"][0], - queryParams: Required>, -): Promise { - logger.debug("fetchEntitiesRange()"); - - const {loadRange} = (await resolveProviderApi())["src/api/main/Entity"]; - - return loadRange(typeRef, listId, queryParams.start, queryParams.count, queryParams.reverse); -} - -// TODO consider streaming fetched entities portion to Observable instead of "portionCallback" -export async function fetchEntitiesRangeUntilTheEnd>( - typeRef: TypeRef, - listId: T["_id"][0], - {start, count}: Required>, - portionCallback: (entities: T[]) => Promise, -): Promise { - logger.debug("fetchEntitiesRangeUntilTheEnd()"); - - count = Math.max(1, Math.min(count, 500)); - - const {timestampToGeneratedId, generatedIdToTimestamp} = (await resolveProviderApi())["src/api/common/utils/Encoding"]; - - while (true) { - const entities = await fetchEntitiesRange(typeRef, listId, {start, count, reverse: false}); - - await portionCallback(entities); - - const fetchingCompleted = entities.length < count; - - if (fetchingCompleted) { - break; - } - - const lastEntity = entities[entities.length - 1]; - const currentPortionEndId = Util.resolveInstanceId(lastEntity); - const currentPortionEndTimestamp = generatedIdToTimestamp(currentPortionEndId); - - start = timestampToGeneratedId(currentPortionEndTimestamp + 1); - } -} - -export { - Model, - Util, -}; diff --git a/src/electron-preload/webview/tutanota/lib/rest/model/common.ts b/src/electron-preload/webview/tutanota/lib/rest/model/common.ts deleted file mode 100644 index 906b39f21..000000000 --- a/src/electron-preload/webview/tutanota/lib/rest/model/common.ts +++ /dev/null @@ -1,34 +0,0 @@ -export type Id = T; - -export type IdTuple = [ID1, ID2]; - -export interface BaseEntity { - _id: ID; -} - -export type TypeRefApp = "tutanota" | "sys"; - -export type TypeRefType = - | "File" - | "MailBody" - | "MailboxGroupRoot" - | "MailBox" - | "MailFolder" - | "Mail" - | "ConversationEntry" - | "ContactList" - | "Contact" - | "EntityEventBatch"; - -export interface TypeRef> { - _type: T; - app: TypeRefApp; - type: TypeRefType; -} - -export interface RequestParams { - ids?: Id[]; - start?: string; - count?: number; - reverse?: boolean; -} diff --git a/src/electron-preload/webview/tutanota/lib/rest/model/constants.ts b/src/electron-preload/webview/tutanota/lib/rest/model/constants.ts deleted file mode 100644 index 70ca5b399..000000000 --- a/src/electron-preload/webview/tutanota/lib/rest/model/constants.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {buildEnumBundle} from "src/shared/util"; - -export const GROUP_TYPE = buildEnumBundle({ - User: "0", - Admin: "1", - Team: "2", - Customer: "3", - External: "4", - Mail: "5", - Contact: "6", - File: "7", - LocalAdmin: "8", -} as const); - -export const MAIL_STATE = buildEnumBundle({ - DRAFT: "0", - SENT: "1", - RECEIVED: "2", - SENDING: "3", -} as const); diff --git a/src/electron-preload/webview/tutanota/lib/rest/model/index.ts b/src/electron-preload/webview/tutanota/lib/rest/model/index.ts deleted file mode 100644 index 9f2626154..000000000 --- a/src/electron-preload/webview/tutanota/lib/rest/model/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./common"; -export * from "./constants"; -export * from "./response"; -export * from "./type-ref"; diff --git a/src/electron-preload/webview/tutanota/lib/rest/model/response.ts b/src/electron-preload/webview/tutanota/lib/rest/model/response.ts deleted file mode 100644 index d40273253..000000000 --- a/src/electron-preload/webview/tutanota/lib/rest/model/response.ts +++ /dev/null @@ -1,152 +0,0 @@ -import {BaseEntity, Id, IdTuple, TypeRefApp, TypeRefType} from "./common"; -import { - CONTACT_ADDRESS_TYPE, - CONTACT_PHONE_NUMBER_TYPE, - CONTACT_SOCIAL_TYPE, - CONVERSATION_TYPE, - MAIL_FOLDER_TYPE, - OPERATION_TYPE, - REPLY_TYPE, -} from "src/shared/model/database"; -import {GROUP_TYPE, MAIL_STATE} from "./constants"; -import {NumberString} from "src/shared/model/common"; - -export interface User extends BaseEntity { - memberships: GroupMembership[]; - userGroup: GroupMembership; -} - -export interface Group extends BaseEntity {} - -export interface GroupMembership extends BaseEntity { - groupType: TypeRecord[keyof TypeRecord]; - group: Group["_id"]; -} - -export interface MailboxGroupRoot extends BaseEntity { - mailbox: MailBox["_id"]; -} - -export interface MailBox extends BaseEntity { - systemFolders?: MailFolderRef; - receivedAttachments: MailBoxReceivedAttachment["_id"]; -} - -export interface MailBoxReceivedAttachment extends BaseEntity {} - -export interface MailFolderRef extends BaseEntity { - folders: Id; -} - -export interface MailFolder extends BaseEntity { - folderType: TypeRecord[keyof TypeRecord]; - mails: MailList["_id"]; - subFolders: Id; - name: string; -} - -export interface MailList extends BaseEntity {} - -export interface ConversationEntry extends BaseEntity { - conversationType: TypeRecord[keyof TypeRecord]; - messageId: string; - mail?: Mail["_id"]; - previous?: ConversationEntry["_id"]; -} - -// tslint:disable-next-line:max-line-length -export interface Mail extends BaseEntity<[MailList["_id"], Id]> { - sentDate: NumberString; // timestamp; - receivedDate: NumberString; // timestamp; - movedTime?: NumberString; // timestamp; - subject: string; - body: MailBody["_id"]; - sender: MailAddress; - toRecipients: MailAddress[]; - ccRecipients: MailAddress[]; - bccRecipients: MailAddress[]; - attachments: Array; - unread: "0" | "1"; - state: StateRecord[keyof StateRecord]; - conversationEntry: ConversationEntry["_id"]; - confidential: boolean; - replyType: ReplyRecord[keyof ReplyRecord]; -} - -export interface MailAddress extends BaseEntity { - address: string; - name: string; -} - -export interface File extends BaseEntity<[MailBox["receivedAttachments"], Id]> { - mimeType?: string; - name: string; - size: NumberString; -} - -export type MailBody = BaseEntity - & ( - | { text: string; compressedText: null | undefined; } - | { text: null | undefined; compressedText: string; } - ); - -export interface ContactList extends BaseEntity { - contacts: Id; // TODO defined as Id -} - -export interface Contact extends BaseEntity<[ContactList["contacts"], Id]> { - comment: string; - company: string; - firstName: string; - lastName: string; - nickname?: string; - role: string; - title?: string; - addresses: ContactAddress[]; - birthday?: Birthday; - mailAddresses: ContactMailAddress[]; - phoneNumbers: ContactPhoneNumber[]; - socialIds: ContactSocialId[]; -} - -export interface ContactAddress extends BaseEntity { - type: TypeRecord[keyof TypeRecord]; - customTypeName: string; - address: string; -} - -export interface ContactMailAddress extends BaseEntity { - type: TypeRecord[keyof TypeRecord]; - customTypeName: string; - address: string; -} - -export interface ContactPhoneNumber extends BaseEntity { - type: TypeRecord[keyof TypeRecord]; - customTypeName: string; - number: string; -} - -export interface ContactSocialId extends BaseEntity { - type: TypeRecord[keyof TypeRecord]; - customTypeName: string; - socialId: string; -} - -export interface Birthday extends BaseEntity { - day: NumberString; - month: NumberString; - year?: NumberString; -} - -export interface EntityUpdate extends BaseEntity { - application: TypeRefApp; - type: TypeRefType; - instanceId: Id; - instanceListId: Id; - operation: OperationRecord[keyof OperationRecord]; -} - -export interface EntityEventBatch extends BaseEntity<[GroupMembership["_id"], Id]> { - events: EntityUpdate[]; -} diff --git a/src/electron-preload/webview/tutanota/lib/rest/model/type-ref.ts b/src/electron-preload/webview/tutanota/lib/rest/model/type-ref.ts deleted file mode 100644 index 666066e68..000000000 --- a/src/electron-preload/webview/tutanota/lib/rest/model/type-ref.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {BaseEntity, Id, IdTuple, TypeRef, TypeRefApp} from "./common"; -import { - Contact, - ContactList, - ConversationEntry, - EntityEventBatch, - File, - Mail, - MailBody, - MailBox, - MailFolder, - MailboxGroupRoot, -} from "./response"; - -// tslint:disable:variable-name - -export const FileTypeRef = buildTypeRef("File", "tutanota"); -export const MailBodyTypeRef = buildTypeRef("MailBody", "tutanota"); -export const MailboxGroupRootTypeRef = buildTypeRef("MailboxGroupRoot", "tutanota"); -export const MailBoxTypeRef = buildTypeRef("MailBox", "tutanota"); -export const MailFolderTypeRef = buildTypeRef("MailFolder", "tutanota"); -export const ConversationEntryTypeRef = buildTypeRef("ConversationEntry", "tutanota"); -export const MailTypeRef = buildTypeRef("Mail", "tutanota"); -export const ContactListTypeRef = buildTypeRef("ContactList", "tutanota"); -export const ContactTypeRef = buildTypeRef("Contact", "tutanota"); -export const EntityEventBatchTypeRef = buildTypeRef("EntityEventBatch", "sys"); - -// tslint:enable:variable-name - -function buildTypeRef>( - type: Pick, "type">["type"], - app: TypeRefApp, -): TypeRef { - return { - app, - type, - } as any; -} diff --git a/src/electron-preload/webview/tutanota/lib/util.ts b/src/electron-preload/webview/tutanota/lib/util.ts deleted file mode 100644 index fc59db6b9..000000000 --- a/src/electron-preload/webview/tutanota/lib/util.ts +++ /dev/null @@ -1,166 +0,0 @@ -import * as DatabaseModel from "src/shared/model/database"; -import * as Rest from "./rest"; -import {BaseEntity, Id, IdTuple} from "./rest/model"; -import {GROUP_TYPE} from "./rest/model/constants"; -import {WEBVIEW_LOGGERS} from "src/electron-preload/webview/constants"; -import {buildDbPatchRetryPipeline} from "src/electron-preload/webview/util"; -import {curryFunctionMembers} from "src/shared/util"; -import {resolveProviderApi} from "src/electron-preload/webview/tutanota/lib/provider-api"; - -const _logger = curryFunctionMembers(WEBVIEW_LOGGERS.tutanota, "[lib/util]"); - -export const filterSyncingMemberships = ((types: Set) => ({memberships}: Rest.Model.User): Rest.Model.GroupMembership[] => { - return memberships.filter(({groupType}) => types.has(groupType)); -})(new Set([GROUP_TYPE.Mail, GROUP_TYPE.Contact])); - -export const isUpsertOperationType: (v: Unpacked) => boolean = (() => { - const types: ReadonlySet[0]> = new Set( - [ - DatabaseModel.OPERATION_TYPE.CREATE, - DatabaseModel.OPERATION_TYPE.UPDATE, - ], - ); - const result: typeof isUpsertOperationType = (type) => types.has(type); - return result; -})(); - -export async function fetchMailFoldersWithSubFolders(user: Rest.Model.User): Promise { - const logger = curryFunctionMembers(_logger, "fetchMailFoldersWithSubFolders()", JSON.stringify({callId: +new Date()})); - logger.info(); - - const folders: Rest.Model.MailFolder[] = []; - const mailMemberships = user.memberships.filter(({groupType}) => groupType === GROUP_TYPE.Mail); - - for (const {group} of mailMemberships) { - const {mailbox} = await Rest.fetchEntity(Rest.Model.MailboxGroupRootTypeRef, group); - const {systemFolders} = await Rest.fetchEntity(Rest.Model.MailBoxTypeRef, mailbox); - - if (!systemFolders) { - continue; - } - - for (const folder of await Rest.fetchAllEntities(Rest.Model.MailFolderTypeRef, systemFolders.folders)) { - folders.push(folder); - folders.push(...await Rest.fetchAllEntities(Rest.Model.MailFolderTypeRef, folder.subFolders)); - } - } - - logger.verbose(`fetched ${folders.length} folders`); - - return folders; -} - -export async function generateStartId(id?: Rest.Model.Id): Promise { - const api = await resolveProviderApi(); - const {FULL_INDEXED_TIMESTAMP: TIMESTAMP_MIN} = api["src/api/common/TutanotaConstants"]; - const {timestampToGeneratedId, generatedIdToTimestamp} = api["src/api/common/utils/Encoding"]; - const startTimestamp = typeof id === "undefined" ? TIMESTAMP_MIN : generatedIdToTimestamp(id) + 1; - - return timestampToGeneratedId(startTimestamp); -} - -export function sameRefType, R extends Rest.Model.TypeRef>( - refType: R, - {application, type}: Rest.Model.EntityUpdate, -): boolean { - return refType.app === application && refType.type === type; -} - -export function resolveInstanceId>(entity: T): Id { - return Array.isArray(entity._id) ? entity._id[1] : entity._id; -} - -export function resolveListId>(entity: T): Id { - if (!Array.isArray(entity._id) || entity._id.length !== 2) { - throw new Error(`"_id" of the entity is not "IdTuple"/array`); - } - return entity._id[0]; -} - -export function getUserController(): { accessToken: string, user: Rest.Model.User } | null { - const {tutao} = window; - const userController = ( - tutao - && - tutao.logins - && - tutao.logins.getUserController - && - tutao.logins.getUserController() - ); - - return userController - ? userController - : null; -} - -export function isLoggedIn(): boolean { - const userController = getUserController(); - - return !!( - userController - && - userController.accessToken - && - userController.accessToken.length - ); -} - -export function isUpsertUpdate(update: Rest.Model.EntityUpdate) { - return isUpsertOperationType(update.operation); -} - -export const preprocessError: Arguments[0] = (rawError: any) => { - const {name, message}: { name?: unknown; message?: unknown } = Object(rawError); - const retriable = ( - !navigator.onLine - || - name === "ConnectionError" - || - String(message).includes("ConnectionError:") - || - String(message).includes("Reached timeout") - || - (name === "ServiceUnavailableError" && String(message).startsWith("503")) - ); - - for (const prop in Object(rawError)) { - if (Object(rawError).hasOwnProperty(prop) && typeof rawError[prop] === "string") { - rawError[prop] = depersonalizeLoggedData(rawError[prop]); - } - } - - rawError.stack = depersonalizeLoggedData( - String(rawError.stack), - ); - - return { - error: rawError, - retriable, - skippable: retriable, - }; -}; - -export function depersonalizeLoggedData(value: string): string { - // tutanota's IDs include normally include "/sdfjbjsdfjh----1" | "sdfjbjsdfjh-2s-0"; - const mapSubPart = (subPart: string) => { - const sensitive = /-[\d]+$/.test( - String(subPart) - .replace(/[\r\t\n]/g, ""), - ); - return sensitive - ? "" - : subPart; - }; - return value - .split("/") - .map((part) => { - return /-[\d]+/.test(part) - ? part - .split(" ") - .map(mapSubPart) - .join(" ") - : part; - }) - .join("/"); -} diff --git a/src/electron-preload/webview/tutanota/tsconfig.json b/src/electron-preload/webview/tutanota/tsconfig.json deleted file mode 100644 index d791dd2e2..000000000 --- a/src/electron-preload/webview/tutanota/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "files": [ - "./index.ts", - "./typings.d.ts" - ] -} diff --git a/src/electron-preload/webview/tutanota/types.ts b/src/electron-preload/webview/tutanota/types.ts deleted file mode 100644 index 9ed160761..000000000 --- a/src/electron-preload/webview/tutanota/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as Rest from "src/electron-preload/webview/tutanota/lib/rest"; - -export interface TutanotaWindow { - SystemJS: SystemJSLoader.System; - tutao?: { - m: { - route: import("mithril").Route; - }; - logins?: { - getUserController?: () => { accessToken: string, user: Rest.Model.User }; - }; - }; -} diff --git a/src/electron-preload/webview/tutanota/typings.d.ts b/src/electron-preload/webview/tutanota/typings.d.ts deleted file mode 100644 index d01fdc459..000000000 --- a/src/electron-preload/webview/tutanota/typings.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {TutanotaWindow} from "src/electron-preload/webview/tutanota/types"; - -declare global { - interface Window extends TutanotaWindow {} -} - -declare var window: Window; diff --git a/src/electron-preload/webview/util.ts b/src/electron-preload/webview/util.ts index 5b3e63823..753d14958 100644 --- a/src/electron-preload/webview/util.ts +++ b/src/electron-preload/webview/util.ts @@ -229,10 +229,6 @@ function triggerChangeEvent(input: HTMLInputElement) { const changeEvent = document.createEvent("HTMLEvents"); changeEvent.initEvent("change", true, false); input.dispatchEvent(changeEvent); - // tutanota (mithril) - const inputEvent = document.createEvent("Event"); - inputEvent.initEvent("input", true, false); - input.dispatchEvent(inputEvent); } export async function persistDatabasePatch( diff --git a/src/shared/api/main.ts b/src/shared/api/main.ts index d4eb307aa..f29405988 100644 --- a/src/shared/api/main.ts +++ b/src/shared/api/main.ts @@ -42,7 +42,7 @@ export const ENDPOINTS_DEFINITION = { dbPatch: ActionType.Promise["metadata"], "type"> | Skip["metadata"], "type"> }, + & { metadata: Skip["metadata"], "type"> }, DbModel.FsDbAccount["metadata"]>(), dbGetAccountMetadata: ActionType.Promise(), diff --git a/src/shared/api/webview/tutanota.ts b/src/shared/api/webview/tutanota.ts deleted file mode 100644 index 261da9fa5..000000000 --- a/src/shared/api/webview/tutanota.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {ScanService, createWebViewApiService} from "electron-rpc-api"; - -import {NotificationsTutanota} from "src/shared/model/account"; -import {buildLoggerBundle} from "src/electron-preload/util"; -import {buildWebViewApiDefinition, channel} from "./common"; - -export type TutanotaNotificationOutput = Partial & Partial<{ batchEntityUpdatesCounter: number }>; - -export type TutanotaScanApi = ScanService; - -export type TutanotaApi = TutanotaScanApi["ApiClient"]; - -export const TUTANOTA_IPC_WEBVIEW_API_DEFINITION = { - ...buildWebViewApiDefinition<"tutanota", TutanotaNotificationOutput>(), -} as const; - -export const TUTANOTA_IPC_WEBVIEW_API = createWebViewApiService({ - channel, - apiDefinition: TUTANOTA_IPC_WEBVIEW_API_DEFINITION, - logger: buildLoggerBundle("[IPC_WEBVIEW_API:tutanota]"), -}); diff --git a/src/shared/constants.ts b/src/shared/constants.ts index df3f3deea..08436289d 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -62,11 +62,6 @@ export const PROVIDER_REPO: Record { type: Type; @@ -16,8 +16,7 @@ export interface GenericAccountConfig; -export type AccountConfigTutanota = GenericAccountConfig<"tutanota", "password" | "twoFactorCode">; -export type AccountConfig = Extract; +export type AccountConfig = Extract; export interface GenericNotifications { title: string; @@ -26,6 +25,5 @@ export interface GenericNotifications; export type NotificationsProtonmail = GenericNotifications<"unknown" | "login" | "login2fa" | "unlock">; -export type Notifications = NotificationsProtonmail | NotificationsTutanota; +export type Notifications = NotificationsProtonmail; diff --git a/src/shared/model/database/constants.ts b/src/shared/model/database/constants.ts index 30008ca7e..cea0b1dff 100644 --- a/src/shared/model/database/constants.ts +++ b/src/shared/model/database/constants.ts @@ -39,7 +39,6 @@ export const MAIL_STATE = buildEnumBundle({ SENT: "1", RECEIVED: "2", PROTONMAIL_INBOX_AND_SENT: "100", - TUTANOTA_SENDING: "101", } as const); export const REPLY_TYPE = buildEnumBundle({ diff --git a/src/shared/model/database/index.ts b/src/shared/model/database/index.ts index 1fdbd3b1d..b6a52e797 100644 --- a/src/shared/model/database/index.ts +++ b/src/shared/model/database/index.ts @@ -143,17 +143,12 @@ interface GenericDb { Readonly>>; } -interface TutanotaMetadataPart { - groupEntityEventBatchIds: Record; -} - interface ProtonmailMetadataPart { latestEventId: string; // Rest.Model.Event["EventID"] } export type FsDb = & Partial - & GenericDb<"tutanota", TutanotaMetadataPart> & GenericDb<"protonmail", ProtonmailMetadataPart>; export type FsDbAccount = FsDb["accounts"][T][string]; diff --git a/src/shared/model/electron.ts b/src/shared/model/electron.ts index 014da3e9f..2bed3fed7 100644 --- a/src/shared/model/electron.ts +++ b/src/shared/model/electron.ts @@ -4,14 +4,12 @@ import {AccountType} from "src/shared/model/account"; import {IPC_MAIN_API} from "src/shared/api/main"; import {Logger} from "src/shared/model/common"; import {PROTONMAIL_IPC_WEBVIEW_API} from "src/shared/api/webview/protonmail"; -import {TUTANOTA_IPC_WEBVIEW_API} from "src/shared/api/webview/tutanota"; import {registerDocumentClickEventListener} from "src/electron-preload/events-handling"; export type ElectronExposure = Readonly<{ buildIpcMainClient: typeof IPC_MAIN_API.client; buildIpcWebViewClient: { protonmail: typeof PROTONMAIL_IPC_WEBVIEW_API.client; - tutanota: typeof TUTANOTA_IPC_WEBVIEW_API.client; }; registerDocumentClickEventListener: typeof registerDocumentClickEventListener; rollingRateLimiter: (options: InMemoryOptions) => SyncOrAsyncLimiter, diff --git a/src/shared/util.ts b/src/shared/util.ts index 1d3e54a4c..b31c49b61 100644 --- a/src/shared/util.ts +++ b/src/shared/util.ts @@ -293,9 +293,7 @@ export function isDatabaseBootstrapped( return false; } - return metadata.type === "protonmail" - ? Boolean(metadata.latestEventId) - : Boolean(Object.keys(metadata.groupEntityEventBatchIds || {}).length); + return Boolean(metadata.latestEventId); } export function getRandomInt(min: number, max: number): number { diff --git a/src/web/about/index.scss b/src/web/about/index.scss index 508dd60ac..e11d8ce2e 100644 --- a/src/web/about/index.scss +++ b/src/web/about/index.scss @@ -35,6 +35,10 @@ body { text-align: center; } + h1 ~ p { + padding: 0 50px; + } + img { width: 128px; height: 128px; diff --git a/src/web/browser-window/app/_accounts/account-title.component.html b/src/web/browser-window/app/_accounts/account-title.component.html index 7a2c500e4..b2ff323f9 100644 --- a/src/web/browser-window/app/_accounts/account-title.component.html +++ b/src/web/browser-window/app/_accounts/account-title.component.html @@ -4,7 +4,6 @@ class="btn-group" > - ["client"]>[0], undefined>>["options"]; @@ -65,7 +64,7 @@ export class ElectronService implements OnDestroy { ) { // TODO TS: improve WebView API client resolve and use type-safety const buildIpcWebViewClient = __ELECTRON_EXPOSURE__.buildIpcWebViewClient[type]; - const client: ReturnType & ReturnType = + const client: ReturnType = buildIpcWebViewClient(webView, {options: this.buildApiCallOptions(options)}) as any; // TODO TS: get rid of typecasting // TODO consider removing "ping" API or pinging once per "webView", keeping state in WeakMap? diff --git a/src/web/browser-window/app/_db-view/db-view-mails.component.html b/src/web/browser-window/app/_db-view/db-view-mails.component.html index 4aa305584..a38e0b4b0 100644 --- a/src/web/browser-window/app/_db-view/db-view-mails.component.html +++ b/src/web/browser-window/app/_db-view/db-view-mails.component.html @@ -5,7 +5,6 @@