Skip to content

Commit aaf0235

Browse files
committed
enable support for javascript-based search filters, closes #257
* filter function typed in TypeScript * Monaco Editor used as a code editor
1 parent 0dd8a28 commit aaf0235

34 files changed

+961
-347
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ Some Linux package types are available for installing from the repositories (`Pa
3131
- :octocat: **Open Source**.
3232
- :gear: **Reproducible builds**. See details in [#183](https://github.com/vladimiry/ElectronMail/issues/183).
3333
- :gear: **Cross platform**. The app works on Linux/OSX/Windows platforms. Binary installation packages located [here](https://github.com/vladimiry/ElectronMail/releases).
34-
- :mag_right: **Full-text search**. Including email body content scanning capability. Enabled with [v2.2.0](https://github.com/vladimiry/ElectronMail/releases/tag/v2.2.0) release. See the respective [issue](https://github.com/vladimiry/ElectronMail/issues/92) for details.
34+
- :mag_right: **Full-text search**. Including email **body content** scanning capability. Enabled with [v2.2.0](https://github.com/vladimiry/ElectronMail/releases/tag/v2.2.0) release. See the respective [issue](https://github.com/vladimiry/ElectronMail/issues/92) for details.
35+
- :mag_right: **JavaScript-based/unlimited messages filtering**. Enabled since [v4.11.0](https://github.com/vladimiry/ElectronMail/releases/tag/v4.11.0) release. See the respective [#257](https://github.com/vladimiry/ElectronMail/issues/257) for details. Requires [local store](https://github.com/vladimiry/ElectronMail/wiki/FAQ) feature to be enabled.
3536
- :package: **Offline access to the email messages** (attachments content not stored locally, but emails body content). The [local store](https://github.com/vladimiry/ElectronMail/wiki/FAQ) feature enables storing your messages in the encrypted `database.bin` file (see [FAQ](https://github.com/vladimiry/ElectronMail/wiki/FAQ) for file purpose details). So the app allows you to view your messages offline, running full-text search against them, exporting them to EML files. etc. Enabled since [v2.0.0](https://github.com/vladimiry/ElectronMail/releases/tag/v2.0.0) release.
3637
- :mailbox: **Multi accounts** support including supporting individual [API entry points](https://github.com/vladimiry/ElectronMail/issues/29). For example, you can force the specific email account added in the app connect to the email provider via the [Tor](https://www.torproject.org/) only by selecting the `https://protonirockerxow.onion/` API entry point in the dropdown list and configuring a proxy as described in [this](https://github.com/vladimiry/ElectronMail/issues/113#issuecomment-529130116) message.
3738
- :unlock: **Automatic login into the app** with a remembered the system keychain remembered master ([keep me signed in](images/keep-me-signed-in.png) feature). Integration with as a system keychain is done with the [keytar](https://github.com/atom/node-keytar) module. By the way, on Linux [KeePassXC](https://github.com/keepassxreboot/keepassxc) implements the [Secret Service](https://specifications.freedesktop.org/secret-service/latest/) interface and so it can be acting as a system keychain (for details, see the "automatic login into the app"-related point in the [FAQ](https://github.com/vladimiry/ElectronMail/wiki/FAQ)).
@@ -47,7 +48,7 @@ Some Linux package types are available for installing from the repositories (`Pa
4748
- :bell: **Native notifications** for individual accounts clicking on which focuses the app window and selects respective account in the accounts list.
4849
- :calendar: **Calendar notifications / alarms** regardless of the open page (mail/calendar/settings/account/drive). The opt-in feature has been enabled since [v4.9.0](https://github.com/vladimiry/ElectronMail/releases/tag/v4.9.0). See [#229](https://github.com/vladimiry/ElectronMail/issues/229) for details.
4950
- :sunglasses: **Making all email "read"** in a single mouse click. Enabled since [v3.8.0](https://github.com/vladimiry/ElectronMail/releases/tag/v3.8.0). Requires [local store](https://github.com/vladimiry/ElectronMail/wiki/FAQ) feature to be enabled.
50-
- :sunglasses: **Routing images through proxy**. The opt-in feature has been enabled since [v4.9.0](https://github.com/vladimiry/ElectronMail/releases/tag/v4.9.0). See [#312](https://github.com/vladimiry/ElectronMail/issues/312) for details. Requires [local store](https://github.com/vladimiry/ElectronMail/wiki/FAQ) feature to be enabled.
51+
- :sunglasses: **Routing images through proxy**. The opt-in feature has been enabled since [v4.9.0](https://github.com/vladimiry/ElectronMail/releases/tag/v4.9.0). See [#312](https://github.com/vladimiry/ElectronMail/issues/312) for details.
5152
- :sunglasses: **Batch mails removing** bypassing the trash. Enabled since [v4.9.0](https://github.com/vladimiry/ElectronMail/releases/tag/v4.9.0). Requires [local store](https://github.com/vladimiry/ElectronMail/wiki/FAQ) feature to be enabled.
5253
- :sunglasses: **Batch mails moving between folders**. Enabled since [v4.5.0](https://github.com/vladimiry/ElectronMail/releases/tag/v4.5.0). Requires [local store](https://github.com/vladimiry/ElectronMail/wiki/FAQ) feature to be enabled.
5354

package.json

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "electron-mail",
33
"description": "Unofficial ProtonMail Desktop App",
4-
"version": "4.10.3",
4+
"version": "4.11.0",
55
"author": "Vladimir Yakovlev <[email protected]>",
66
"license": "MIT",
77
"homepage": "https://github.com/vladimiry/ElectronMail",
@@ -67,9 +67,9 @@
6767
"assets:dev": "npm-run-all assets:copy:dev assets:webclient:dev",
6868
"assets:copy": "cpx \"./src/assets/dist/**/*\" ./app/assets",
6969
"assets:copy:dev": "cpx \"./src/assets/dist/**/*\" ./app-dev/assets",
70-
"assets:webclient": "yarn assets:webclient:base ./app/webclient",
71-
"assets:webclient:dev": "yarn assets:webclient:base ./app-dev/webclient",
72-
"assets:webclient:base": "yarn ts-node:shortcut ./scripts/prepare-webclient/protonmail.ts",
70+
"assets:webclient": "yarn assets:webclient:base ./app",
71+
"assets:webclient:dev": "yarn assets:webclient:base ./app-dev",
72+
"assets:webclient:base": "yarn ts-node:shortcut ./scripts/prepare-webclient/index.ts",
7373
"electron-builder:install-app-deps": "electron-builder install-app-deps --arch=x64",
7474
"electron-builder:dir": "electron-builder --x64 --dir",
7575
"electron-builder:dist": "npm exec --package=ts-node -- ts-node --files --require tsconfig-paths/register ./scripts/electron-builder/run-with-default-evn-vars.ts --x64 --publish never",
@@ -101,7 +101,7 @@
101101
"scripts/download-tray-icon-font": "yarn ts-node:shortcut ./scripts/download-tray-icon-font.ts",
102102
"scripts/transfer": "yarn ts-node:shortcut ./scripts/transfer/index.ts",
103103
"ts-node:shortcut": "cross-env TS_NODE_FILES=true ts-node --require tsconfig-paths/register",
104-
"webpack:shortcut": "cross-env TS_NODE_FILES=true npm exec --package=webpack-cli --node-options=\"--require tsconfig-paths/register\" -- webpack"
104+
"webpack:shortcut": "cross-env TS_NODE_PROJECT=./webpack-configs/tsconfig.json TS_NODE_FILES=true npm exec --package=webpack-cli --node-options=\"--require tsconfig-paths/register\" -- webpack"
105105
},
106106
"ava": {
107107
"extensions": [
@@ -145,6 +145,7 @@
145145
"proxy-agent": "4.0.1",
146146
"pure-uuid": "1.6.2",
147147
"pureimage": "0.2.7",
148+
"quickjs-emscripten": "0.11.0",
148149
"reflect-metadata": "0.1.13",
149150
"remeda": "0.0.27",
150151
"rxjs": "6.6.6",
@@ -212,6 +213,7 @@
212213
"cpx2": "3.0.0",
213214
"cross-env": "7.0.3",
214215
"css-loader": "5.1.2",
216+
"dts-generator": "3.0.0",
215217
"electron": "12.0.1",
216218
"electron-builder": "22.10.5",
217219
"escape-string-regexp": "4.0.0",
@@ -227,6 +229,7 @@
227229
"immer": "8.0.1",
228230
"import-sort-cli": "6.0.0",
229231
"import-sort-parser-typescript": "6.0.0",
232+
"imports-loader": "2.0.0",
230233
"jasmine": "3.6.4",
231234
"jsdom": "16.5.1",
232235
"karma": "6.2.0",
@@ -235,6 +238,7 @@
235238
"karma-webpack": "5.0.0",
236239
"lint-staged": "10.5.4",
237240
"mini-css-extract-plugin": "1.3.9",
241+
"monaco-editor": "0.23.0",
238242
"ndx": "1.0.2",
239243
"ndx-query": "1.0.1",
240244
"ngx-bootstrap": "6.2.0",

scripts/lib.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,16 @@ export const catchTopLeventAsync = (asyncFn: () => Promise<unknown>): void => {
223223
process.exit(1);
224224
});
225225
};
226+
227+
export const applyPatch = async ({patchFile, cwd}: { patchFile: string; cwd: string }): Promise<void> => {
228+
await execShell([
229+
"git",
230+
[
231+
"apply",
232+
"--ignore-whitespace",
233+
"--reject",
234+
patchFile,
235+
],
236+
{cwd},
237+
]);
238+
};

scripts/prepare-webclient/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import path from "path";
2+
import pathIsInside from "path-is-inside";
3+
4+
import {CWD_ABSOLUTE_DIR} from "scripts/const";
5+
import {buildProtonClients} from "scripts/prepare-webclient/protonmail";
6+
import {catchTopLeventAsync} from "scripts/lib";
7+
import {resolveProtonMetadata} from "scripts/prepare-webclient/monaco-editor-dts";
8+
9+
const [, , appDestDir] = process.argv;
10+
11+
if (!appDestDir) {
12+
throw new Error("Empty base destination directory argument");
13+
}
14+
15+
if (!pathIsInside(path.resolve(CWD_ABSOLUTE_DIR, appDestDir), CWD_ABSOLUTE_DIR)) {
16+
throw new Error(`Invalid base destination directory argument value: ${appDestDir}`);
17+
}
18+
19+
catchTopLeventAsync(async () => {
20+
await buildProtonClients({destDir: path.join(appDestDir, "./webclient")});
21+
await resolveProtonMetadata({destDir: path.join(appDestDir)});
22+
});
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import fs from "fs";
2+
import fsExtra from "fs-extra";
3+
import path from "path";
4+
5+
import {CONSOLE_LOG} from "scripts/lib";
6+
import {IpcMainServiceScan} from "src/shared/api/main";
7+
import {PROTON_MONACO_EDITOR_DTS_ASSETS_LOCATION} from "src/shared/constants";
8+
import {PROTON_SHARED_MESSAGE_INTERFACE} from "src/shared/proton-apps-constants";
9+
10+
// TODO "require/var-requires"-based import
11+
export const dtsGenerator: { // eslint-disable-line @typescript-eslint/no-unsafe-assignment
12+
default: (options: { baseDir: string, files: string[], out: string }) => Promise<string>
13+
} = require("dts-generator"); // eslint-disable-line @typescript-eslint/no-var-requires
14+
15+
export const resolveProtonMetadata = async (
16+
{destDir}: { destDir: string },
17+
): Promise<IpcMainServiceScan["ApiImplReturns"]["staticInit"]["monacoEditorExtraLibArgs"]> => {
18+
const options = {
19+
system: {
20+
base: "./node_modules/typescript/lib",
21+
in: "./node_modules/typescript/lib/lib.esnext.d.ts",
22+
out: path.join(destDir, PROTON_MONACO_EDITOR_DTS_ASSETS_LOCATION.system),
23+
},
24+
protonMessage: {
25+
base: "./output/git/proton-mail/node_modules/proton-shared",
26+
in: path.join("./output/git/proton-mail/node_modules/proton-shared", PROTON_SHARED_MESSAGE_INTERFACE.projectRelativeFile),
27+
out: path.join(destDir, PROTON_MONACO_EDITOR_DTS_ASSETS_LOCATION.protonMessage),
28+
},
29+
} as const;
30+
31+
// TODO replace "dts-generator" dependency with something capable to combine "./node_modules/typescript/lib/lib.esnext.d.ts"
32+
for (const key of [/* "system", */ "protonMessage"] as const) {
33+
const sourceFile = options[key].in;
34+
if (!fsExtra.pathExistsSync(sourceFile)) {
35+
throw new Error(`The source "${sourceFile}" file doesn't exits.`);
36+
}
37+
await dtsGenerator.default({baseDir: options[key].base, files: [sourceFile], out: options[key].out});
38+
CONSOLE_LOG(`Merged "${options[key].in}" to "${options[key].out}"`);
39+
}
40+
41+
// TODO drop custom "./node_modules/typescript/lib/lib.esnext.d.ts" combining when "dts-generator" gets replaced
42+
{
43+
const referenceTagRe = /\/\/\/[\s\t]+<reference[\s\t]+lib=["']+(.*)["']+[\s\t]+\/>/;
44+
const mergedFiles: string[] = [];
45+
const extractContent = (file: string): string => {
46+
const lines = fs.readFileSync(file).toString().split("\n");
47+
const resultLines: string[] = [`// === file: ${file}`];
48+
for (const line of lines) {
49+
const match = referenceTagRe.exec(line);
50+
const libName = match && match[1];
51+
if (libName) {
52+
resultLines.push(
53+
extractContent(path.join(path.dirname(file), `lib.${libName}.d.ts`)),
54+
);
55+
} else {
56+
resultLines.push(line);
57+
}
58+
}
59+
mergedFiles.push(file);
60+
return resultLines.join("\n");
61+
};
62+
fsExtra.ensureDirSync(path.dirname(options.system.out));
63+
fs.writeFileSync(options.system.out, extractContent(options.system.in));
64+
CONSOLE_LOG(`Merged to "${options.system.out}" files:`, mergedFiles);
65+
}
66+
67+
return {
68+
system: [
69+
fs.readFileSync(options.system.out).toString(),
70+
`in-memory:${options.system.in}`,
71+
],
72+
protonMessage: [
73+
(
74+
fs.readFileSync(options.protonMessage.out).toString()
75+
+
76+
`declare const mail: Omit<import("lib/interfaces/mail/Message").Message, "Body"> & {Body: string};`
77+
),
78+
`in-memory:${PROTON_SHARED_MESSAGE_INTERFACE.url}`,
79+
],
80+
};
81+
};

scripts/prepare-webclient/lib.ts renamed to scripts/prepare-webclient/protonmail-lib.ts

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import fs from "fs";
22
import fsExtra from "fs-extra";
33
import path from "path";
4-
import pathIsInside from "path-is-inside";
54

65
import {CONSOLE_LOG, execShell, resolveGitCommitInfo, resolveGitOutputBackupDir} from "scripts/lib";
7-
import {CWD_ABSOLUTE_DIR, GIT_CLONE_ABSOLUTE_DIR} from "scripts/const";
6+
import {GIT_CLONE_ABSOLUTE_DIR} from "scripts/const";
87
import {PROVIDER_REPO_MAP} from "src/shared/proton-apps-constants";
98
import {RUNTIME_ENV_CI_PROTON_CLIENTS_ONLY, WEB_CLIENTS_BLANK_HTML_FILE_NAME} from "src/shared/constants";
109

@@ -26,16 +25,6 @@ const reposOnlyFilter: DeepReadonly<{ value: Array<keyof typeof PROVIDER_REPO_MA
2625
return {value: result, envVariableName};
2726
})();
2827

29-
const [, , BASE_DEST_DIR] = process.argv;
30-
31-
if (!BASE_DEST_DIR) {
32-
throw new Error("Empty base destination directory argument");
33-
}
34-
35-
if (!pathIsInside(path.resolve(CWD_ABSOLUTE_DIR, BASE_DEST_DIR), CWD_ABSOLUTE_DIR)) {
36-
throw new Error(`Invalid base destination directory argument value: ${BASE_DEST_DIR}`);
37-
}
38-
3928
export interface FolderAsDomainEntry<T extends any = any> { // eslint-disable-line @typescript-eslint/no-explicit-any
4029
folderNameAsDomain: string;
4130
options: T;
@@ -75,18 +64,20 @@ export async function executeBuildFlow<T extends FolderAsDomainEntry[], O = Unpa
7564
repoType,
7665
folderAsDomainEntries,
7766
repoRelativeDistDir = PROVIDER_REPO_MAP[repoType].repoRelativeDistDir,
67+
destDir,
7868
destSubFolder,
7969
flow,
8070
}: {
81-
repoType: keyof typeof PROVIDER_REPO_MAP;
82-
folderAsDomainEntries: T;
83-
repoRelativeDistDir?: string;
84-
destSubFolder: string;
71+
repoType: keyof typeof PROVIDER_REPO_MAP
72+
folderAsDomainEntries: T
73+
repoRelativeDistDir?: string
74+
destDir: string
75+
destSubFolder: string
8576
flow: {
86-
postClone?: Flow<O>;
87-
install?: Flow<O>;
88-
build: Flow<O>;
89-
};
77+
postClone?: Flow<O>
78+
install?: Flow<O>
79+
build: Flow<O>
80+
}
9081
},
9182
): Promise<void> {
9283
if (
@@ -101,7 +92,7 @@ export async function executeBuildFlow<T extends FolderAsDomainEntry[], O = Unpa
10192
const repoDir = path.join(GIT_CLONE_ABSOLUTE_DIR, repoType);
10293

10394
for (const folderAsDomainEntry of folderAsDomainEntries) {
104-
const targetDistDir = path.resolve(BASE_DEST_DIR as string, folderAsDomainEntry.folderNameAsDomain, destSubFolder);
95+
const targetDistDir = path.resolve(destDir, folderAsDomainEntry.folderNameAsDomain, destSubFolder);
10596

10697
CONSOLE_LOG(
10798
`Prepare web client build [${repoType}]:`,
@@ -157,19 +148,29 @@ export async function executeBuildFlow<T extends FolderAsDomainEntry[], O = Unpa
157148

158149
const repoDistBackupDir = resolveGitOutputBackupDir({repoType, suffix: `dist-${folderAsDomainEntry.folderNameAsDomain}`});
159150

160-
if (fsExtra.pathExistsSync(repoDistBackupDir)) { // taking dist from the backup
161-
const src = repoDistBackupDir;
162-
const dest = repoDistDir;
163-
CONSOLE_LOG(`Copying backup ${src} to ${dest}`);
164-
await fsExtra.copy(src, dest);
165-
} else { // executing the build
151+
const installModules = async (): Promise<void> => {
166152
if (fsExtra.pathExistsSync(path.resolve(repoDir, "node_modules"))) {
167153
CONSOLE_LOG("Skip dependencies installing");
168154
} else if (flow.install) {
169155
await flow.install(flowOptions);
170156
} else {
171157
await execShell(["npm", ["ci"], {cwd: repoDir}]);
172158
}
159+
};
160+
161+
if (fsExtra.pathExistsSync(repoDistBackupDir)) { // taking dist from the backup
162+
const src = repoDistBackupDir;
163+
const dest = repoDistDir;
164+
165+
CONSOLE_LOG(`Copying backup ${src} to ${dest}`);
166+
await fsExtra.copy(src, dest);
167+
168+
if (repoType === "proton-mail") {
169+
// installing modules since required by "./scripts/prepare-webclient/monaco-editor-dts.ts"
170+
await installModules();
171+
}
172+
} else { // executing the build
173+
await installModules();
173174

174175
if (shouldFailOnBuild) {
175176
throw new Error(`Halting since "${shouldFailOnBuildEnvVarName}" env var has been enabled`);

scripts/prepare-webclient/protonmail.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import fsExtra from "fs-extra";
22
import path from "path";
33

44
import {BINARY_NAME} from "src/shared/constants";
5-
import {catchTopLeventAsync, execShell} from "scripts/lib";
65
import {CWD_ABSOLUTE_DIR} from "scripts/const";
7-
import {executeBuildFlow, FolderAsDomainEntry, printAndWriteFile} from "./lib";
6+
import {FolderAsDomainEntry, executeBuildFlow, printAndWriteFile} from "./protonmail-lib";
87
import {PROVIDER_REPO_MAP, PROVIDER_REPO_NAMES} from "src/shared/proton-apps-constants";
8+
import {applyPatch, execShell} from "scripts/lib";
99

1010
const folderAsDomainEntries: Array<FolderAsDomainEntry<{
1111
configApiParam:
@@ -178,24 +178,12 @@ function resolveWebpackConfigPatchingCode(
178178
return result;
179179
}
180180

181-
const applyPatch = async ({patchFile, cwd}: { patchFile: string; cwd: string }): Promise<void> => {
182-
await execShell([
183-
"git",
184-
[
185-
"apply",
186-
"--ignore-whitespace",
187-
"--reject",
188-
patchFile,
189-
],
190-
{cwd},
191-
]);
192-
};
193-
194-
catchTopLeventAsync(async () => {
181+
export const buildProtonClients = async ({destDir}: { destDir: string }): Promise<void> => {
195182
for (const repoType of PROVIDER_REPO_NAMES) {
196183
await executeBuildFlow({
197184
repoType,
198185
folderAsDomainEntries,
186+
destDir,
199187
destSubFolder: PROVIDER_REPO_MAP[repoType].baseDirName,
200188
flow: {
201189
async install({repoDir}) {
@@ -277,4 +265,4 @@ catchTopLeventAsync(async () => {
277265
},
278266
});
279267
}
280-
});
268+
};

0 commit comments

Comments
 (0)