Skip to content

Commit

Permalink
simplify mail folder type service code
Browse files Browse the repository at this point in the history
  • Loading branch information
vladimiry committed Jul 23, 2018
1 parent f329e9b commit ce49eca
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 122 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "email-securely-app",
"description": "Unofficial desktop app for E2E encrypted email providers",
"version": "1.1.2",
"version": "1.2.0",
"author": "Vladimir Yakovlev <[email protected]>",
"license": "MIT",
"homepage": "https://github.com/vladimiry/email-securely-app",
Expand Down Expand Up @@ -212,7 +212,7 @@
"wait-on": "2.1.0",
"webpack": "4.16.1",
"webpack-cli": "3.1.0",
"webpack-dev-server": "3.1.4",
"webpack-dev-server": "3.1.5",
"webpack-merge": "4.1.3",
"webpack-node-externals": "1.7.2",
"zone.js": "0.8.26"
Expand Down
154 changes: 71 additions & 83 deletions src/electron-preload/webview/tutanota/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logger from "electron-log";
import {authenticator} from "otplib";
import {distinctUntilChanged, map, switchMap} from "rxjs/operators";
import {EMPTY, from, interval, merge, Observable, Subscriber, throwError} from "rxjs";
import {EMPTY, from, interval, merge, Observable, Subscriber} from "rxjs";

import {AccountNotificationType, WebAccountTutanota} from "src/shared/model/account";
import {fetchEntitiesRange} from "src/electron-preload/webview/tutanota/lib/rest";
Expand All @@ -16,8 +16,6 @@ import {MailFolderTypeService} from "src/shared/util";

const WINDOW = window as any;

delete WINDOW.Notification;

resolveWebClientApi()
.then((webClientApi) => {
delete WINDOW.Notification;
Expand Down Expand Up @@ -111,95 +109,85 @@ function bootstrapApi(webClientApi: WebClientApi) {
})()),

notification: ({entryUrl}) => {
try {
const observables = [
interval(NOTIFICATION_LOGGED_IN_POLLING_INTERVAL).pipe(
map(() => isLoggedIn()),
distinctUntilChanged(),
map((loggedIn) => ({loggedIn})),
),

// TODO listen for location.href change instead of starting polling interval
interval(NOTIFICATION_PAGE_TYPE_POLLING_INTERVAL).pipe(
switchMap(() => from((async () => {
const url = getLocationHref();
const pageType: (Pick<AccountNotificationType<WebAccountTutanota>, "pageType">)["pageType"] = {
url,
type: "undefined",
};
const loginUrlDetected = (url === `${entryUrl}/login` || url.startsWith(`${entryUrl}/login?`));

if (loginUrlDetected && !isLoggedIn()) {
let twoFactorElements;

try {
twoFactorElements = await waitElements(login2FaWaitElementsConfig, {attemptsLimit: 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}) => prev.type === curr.type),
),
const observables = [
interval(NOTIFICATION_LOGGED_IN_POLLING_INTERVAL).pipe(
map(() => isLoggedIn()),
distinctUntilChanged(),
map((loggedIn) => ({loggedIn})),
),

// TODO listen for location.href change instead of starting polling interval
interval(NOTIFICATION_PAGE_TYPE_POLLING_INTERVAL).pipe(
switchMap(() => from((async () => {
const url = getLocationHref();
const pageType: (Pick<AccountNotificationType<WebAccountTutanota>, "pageType">)["pageType"] = {
url,
type: "undefined",
};
const loginUrlDetected = (url === `${entryUrl}/login` || url.startsWith(`${entryUrl}/login?`));

// TODO listen for "unread" change instead of starting polling interval
Observable.create((observer: Subscriber<{ unread: number }>) => {
const intervalHandler = async () => {
const controller = getUserController();
if (loginUrlDetected && !isLoggedIn()) {
let twoFactorElements;

if (!controller) {
return;
try {
twoFactorElements = await waitElements(login2FaWaitElementsConfig, {attemptsLimit: 1});
} catch (e) {
// NOOP
}

const folders = await fetchUserFoldersWithSubFolders(controller.user);
const inboxFolder = folders
.find(({folderType}) => MailFolderTypeService.testValue(folderType as any, "inbox", false));
const twoFactorCodeVisible = twoFactorElements
&& twoFactorElements.input().offsetParent
&& twoFactorElements.button().offsetParent;

if (!inboxFolder || !isLoggedIn() || !navigator.onLine) {
return;
if (twoFactorCodeVisible) {
pageType.type = "login2fa";
} else {
pageType.type = "login";
}

const {GENERATED_MAX_ID} = webClientApi["src/api/common/EntityFunctions"];
const emails = await fetchEntitiesRange(
MailTypeRef,
inboxFolder.mails,
{
count: 50,
reverse: true,
start: GENERATED_MAX_ID,
},
);
const unread = emails.reduce((sum, mail) => sum + Number(mail.unread), 0);

observer.next({unread});
};

setInterval(
intervalHandler,
ONE_SECOND_MS * 60,
}

return {pageType};
})())),
distinctUntilChanged(({pageType: prev}, {pageType: curr}) => prev.type === curr.type),
),

// TODO listen for "unread" change instead of starting polling interval
Observable.create((observer: Subscriber<{ unread: number }>) => {
const notifyUnreadValue = async () => {
const controller = getUserController();

if (!controller) {
return;
}

const folders = await fetchUserFoldersWithSubFolders(controller.user);
const inboxFolder = folders.find(({folderType}) => MailFolderTypeService.testValue(folderType, "inbox"));

if (!inboxFolder || !isLoggedIn() || !navigator.onLine) {
return;
}

const {GENERATED_MAX_ID} = webClientApi["src/api/common/EntityFunctions"];
const emails = await fetchEntitiesRange(
MailTypeRef,
inboxFolder.mails,
{
count: 50,
reverse: true,
start: GENERATED_MAX_ID,
},
);
const unread = emails.reduce((sum, mail) => sum + Number(mail.unread), 0);

// initial checks after notifications subscription happening
setTimeout(intervalHandler, ONE_SECOND_MS * 15);
}),
];
observer.next({unread});
};

return merge(...observables);
} catch (error) {
return throwError(error);
}
setInterval(notifyUnreadValue, ONE_SECOND_MS * 60);
setTimeout(notifyUnreadValue, ONE_SECOND_MS * 15);
}),
];

return merge(...observables);
},
};

Expand Down
2 changes: 1 addition & 1 deletion src/electron-preload/webview/tutanota/lib/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ function formDatabaseMailModel(
body: body.text,
folder: {
raw: JSON.stringify(folder),
type: MailFolderTypeService.parseValue(folder.folderType),
type: MailFolderTypeService.parseValueStrict(folder.folderType),
name: folder.name,
},
sender: formDatabaseAddressModel(mail.sender),
Expand Down
1 change: 1 addition & 0 deletions src/shared/model/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ export type EntityRecord = Record<"Mail", ClassType<Mail>>;
export type EntityTable = keyof EntityRecord;

export type MailFolderTypeValue = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
export type MailFolderTypeStringifiedValue = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8";
export type MailFolderTypeTitle = "custom" | "inbox" | "sent" | "trash" | "archive" | "spam" | "draft";
43 changes: 20 additions & 23 deletions src/shared/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {AccountConfig} from "./model/account";
import {BaseConfig, Config} from "./model/options";
import {MailFolderTypeTitle, MailFolderTypeValue} from "./model/database";
import {MailFolderTypeStringifiedValue, MailFolderTypeTitle, MailFolderTypeValue} from "./model/database";
import {StatusCode, StatusCodeError} from "./model/error";
import {WEBVIEW_SRC_WHITELIST} from "./constants";

Expand Down Expand Up @@ -48,40 +48,37 @@ export const MailFolderTypeService = (() => {
...accumulator,
[value]: key as MailFolderTypeTitle,
}), {} as Record<MailFolderTypeValue, MailFolderTypeTitle>);
const titles = Object.keys(mappedByTitle);
const values = Object.values(mappedByTitle);
const parseValue = (raw: any): MailFolderTypeValue => {
const result = Number(raw) as MailFolderTypeValue;
if (!values.includes(result)) {
throw new Error(`Invalid mail folder type value: ${result}`);

// TODO consider using some module for building custom errors
class InvalidArgumentError extends Error {
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
return result;
};
const parseTitle = (raw: any): MailFolderTypeTitle => {
const result = String(raw) as MailFolderTypeTitle;
if (!titles.includes(result)) {
throw new Error(`Invalid mail folder type title: ${result}`);
}

function parseValueStrict(value: MailFolderTypeValue | MailFolderTypeStringifiedValue): MailFolderTypeValue {
const result = Number(value) as MailFolderTypeValue;
if (!values.includes(result)) {
throw new InvalidArgumentError(`Invalid mail folder type value: ${result}`);
}
return result;
};
const getValueByTitle = (input: MailFolderTypeTitle): MailFolderTypeValue => mappedByTitle[parseTitle(input)];
const getTitleByValue = (input: MailFolderTypeValue): MailFolderTypeTitle => mappedByValue[parseValue(input)];
const testValue = (value: MailFolderTypeValue, title: MailFolderTypeTitle, strict: boolean = true): boolean => {
}

function testValue(value: MailFolderTypeValue | MailFolderTypeStringifiedValue, title: MailFolderTypeTitle): boolean {
try {
return getTitleByValue(value) === title;
return mappedByValue[parseValueStrict(value)] === title;
} catch (e) {
if (strict) {
if (e instanceof InvalidArgumentError) {
return false;
}
throw e;
}
};
}

return Object.freeze({
parseValue,
parseTitle,
getValueByTitle,
getTitleByValue,
parseValueStrict,
testValue,
});
})();
Loading

0 comments on commit ce49eca

Please sign in to comment.