diff --git a/extension/chrome/settings/initial.htm b/extension/chrome/settings/initial.htm index e59cf22de70..a5c9158bbd6 100644 --- a/extension/chrome/settings/initial.htm +++ b/extension/chrome/settings/initial.htm @@ -36,9 +36,13 @@ Click the button in the corner
+ - + diff --git a/extension/chrome/settings/initial.ts b/extension/chrome/settings/initial.ts index 67e53d17512..4895ad61d28 100644 --- a/extension/chrome/settings/initial.ts +++ b/extension/chrome/settings/initial.ts @@ -8,7 +8,10 @@ import { View } from '../../js/common/view.js'; View.run( class InitialView extends View { public render = async () => { - const browserName = Catch.browser().name === 'chrome' && Number(Catch.browser().v) >= 76 ? 'chrome' : 'firefox'; + const browserName = Catch.browser().name === 'chrome' && Number(Catch.browser().v) >= 76 ? 'chrome' : Catch.browser().name; + if (browserName === 'thunderbird') { + $('#img-setup-arrow').hide(); + } const stepsEl = document.getElementById(`${browserName}-steps`); if (stepsEl) { stepsEl.style.display = 'block'; diff --git a/extension/js/common/browser/browser.ts b/extension/js/common/browser/browser.ts index deab0720340..cd22fa31225 100644 --- a/extension/js/common/browser/browser.ts +++ b/extension/js/common/browser/browser.ts @@ -9,6 +9,7 @@ import { Attachment } from '../core/attachment.js'; import { Catch } from '../platform/catch.js'; import { Dict, Url, UrlParam } from '../core/common.js'; import { GlobalStore } from '../platform/store/global-store.js'; +import { BgUtils } from '../../service_worker/bgutils.js'; export class Browser { public static objUrlCreate = (content: Uint8Array | string) => { @@ -56,19 +57,23 @@ export class Browser { const basePath = chrome.runtime.getURL(`chrome/settings/${path}`); const pageUrlParams = rawPageUrlParams ? JSON.stringify(rawPageUrlParams) : undefined; if (acctEmail || path === 'fatal.htm') { - Browser.openExtensionTab(Url.create(basePath, { acctEmail, page, pageUrlParams })); + await Browser.openExtensionTab(Url.create(basePath, { acctEmail, page, pageUrlParams })); } else if (addNewAcct) { - Browser.openExtensionTab(Url.create(basePath, { addNewAcct })); + await Browser.openExtensionTab(Url.create(basePath, { addNewAcct })); } else { const acctEmails = await GlobalStore.acctEmailsGet(); - Browser.openExtensionTab(Url.create(basePath, { acctEmail: acctEmails[0], page, pageUrlParams })); + await Browser.openExtensionTab(Url.create(basePath, { acctEmail: acctEmails[0], page, pageUrlParams })); } }; - public static openExtensionTab = (url: string) => { - const tab = window.open(url, 'flowcrypt'); - if (tab) { - tab.focus(); + public static openExtensionTab = async (url: string) => { + if (Catch.browser().name === 'thunderbird') { + await BgUtils.openExtensionTab(url); + } else { + const tab = window.open(url, 'flowcrypt'); + if (tab) { + tab.focus(); + } } }; } diff --git a/extension/js/common/browser/env.ts b/extension/js/common/browser/env.ts index 89512c0e290..1a81c2c075c 100644 --- a/extension/js/common/browser/env.ts +++ b/extension/js/common/browser/env.ts @@ -7,7 +7,7 @@ import { Url } from '../core/common.js'; -export type WebMailName = 'gmail' | 'outlook' | 'settings'; +export type WebMailName = 'gmail' | 'thunderbird' | 'outlook' | 'settings'; export type WebMailVersion = 'generic' | 'gmail2020' | 'gmail2022'; export class Env { diff --git a/extension/js/common/inject.ts b/extension/js/common/inject.ts index ad166535a7c..c2aa2ec1a66 100644 --- a/extension/js/common/inject.ts +++ b/extension/js/common/inject.ts @@ -14,6 +14,7 @@ import { PassphraseStore } from './platform/store/passphrase-store.js'; type Host = { gmail: string; + thunderbird: string; outlook: string; settings: string; }; @@ -26,11 +27,13 @@ export class Injector { private container: { [key: string]: Host } = { composeBtnSel: { gmail: 'div.aeN, div.aBO', // .aeN for normal look, .aBO for new look https://github.com/FlowCrypt/flowcrypt-browser/issues/4099 + thunderbird: '', // todo in another issue outlook: 'div._fce_b', settings: '#does_not_have', }, finishSesionBtnSel: { gmail: 'body', + thunderbird: '', // todo in another issue outlook: '#does_not_have', settings: '#settings > div.header', }, diff --git a/extension/js/common/platform/catch.ts b/extension/js/common/platform/catch.ts index cfd00b01565..f0eeb053b15 100644 --- a/extension/js/common/platform/catch.ts +++ b/extension/js/common/platform/catch.ts @@ -149,12 +149,14 @@ export class Catch { } public static browser(): { - name: 'firefox' | 'ie' | 'chrome' | 'opera' | 'safari' | 'unknown'; + name: 'firefox' | 'thunderbird' | 'ie' | 'chrome' | 'opera' | 'safari' | 'unknown'; v: number | undefined; } { // http://stackoverflow.com/questions/4825498/how-can-i-find-out-which-browser-a-user-is-using if (/Firefox[\/\s](\d+\.\d+)/.test(navigator.userAgent)) { return { name: 'firefox', v: Number(RegExp.$1) }; + } else if (/Thunderbird[\/\s](\d+\.\d+)/.test(navigator.userAgent)) { + return { name: 'thunderbird', v: Number(RegExp.$1) }; } else if (/MSIE (\d+\.\d+);/.test(navigator.userAgent)) { return { name: 'ie', v: Number(RegExp.$1) }; } else if (/Chrome[\/\s](\d+\.\d+)/.test(navigator.userAgent)) { diff --git a/extension/js/common/settings.ts b/extension/js/common/settings.ts index 176c9586ccd..74e5432fc9e 100644 --- a/extension/js/common/settings.ts +++ b/extension/js/common/settings.ts @@ -431,7 +431,7 @@ export class Settings { public static async loginWithPopupShowModalOnErr(acctEmail: string, then: () => void = () => undefined) { if (window !== window.top && !chrome.windows) { // Firefox, chrome.windows isn't available in iframes - Browser.openExtensionTab(Url.create(chrome.runtime.getURL(`chrome/settings/index.htm`), { acctEmail })); + await Browser.openExtensionTab(Url.create(chrome.runtime.getURL(`chrome/settings/index.htm`), { acctEmail })); await Ui.modal.info(`Reload after logging in.`); return window.location.reload(); } diff --git a/extension/js/content_scripts/webmail/thunderbird/thunderbird-element-replacer.ts b/extension/js/content_scripts/webmail/thunderbird/thunderbird-element-replacer.ts new file mode 100644 index 00000000000..fc286d6e341 --- /dev/null +++ b/extension/js/content_scripts/webmail/thunderbird/thunderbird-element-replacer.ts @@ -0,0 +1,13 @@ +/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ + +'use strict'; + +import { IntervalFunction, WebmailElementReplacer } from '../generic/webmail-element-replacer'; + +export class ThunderbirdElementReplacer extends WebmailElementReplacer { + public getIntervalFunctions: () => IntervalFunction[]; + public setReplyBoxEditable: () => Promise; + public reinsertReplyBox: (replyMsgId: string) => void; + public scrollToReplyBox: (replyMsgId: string) => void; + public scrollToCursorInReplyBox: (replyMsgId: string, cursorOffsetTop: number) => void; +} diff --git a/extension/js/content_scripts/webmail/thunderbird/thunderbird-webmail-startup.ts b/extension/js/content_scripts/webmail/thunderbird/thunderbird-webmail-startup.ts new file mode 100644 index 00000000000..2e0b16a4d84 --- /dev/null +++ b/extension/js/content_scripts/webmail/thunderbird/thunderbird-webmail-startup.ts @@ -0,0 +1,38 @@ +/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ +import { ClientConfiguration } from '../../../common/client-configuration'; +import { Injector } from '../../../common/inject'; +import { Notifications } from '../../../common/notifications'; +import { contentScriptSetupIfVacant } from '../generic/setup-webmail-content-script'; +import { GmailElementReplacer } from '../gmail/gmail-element-replacer'; +import { ThunderbirdElementReplacer } from './thunderbird-element-replacer'; + +export class ThunderbirdWebmailStartup { + private replacer: GmailElementReplacer; + + public asyncConstructor = async () => { + await contentScriptSetupIfVacant({ + name: 'thunderbird', + variant: undefined, + getUserAccountEmail: () => undefined, // todo, but can start with undefined + getUserFullName: () => undefined, // todo, but can start with undefined + getReplacer: () => new ThunderbirdElementReplacer(), // todo - add this class empty, methods do nothing + start: this.start, + }); + }; + + private start = async ( + acctEmail: string, + clientConfiguration: ClientConfiguration, + injector: Injector, + notifications: Notifications + // factory: XssSafeFactory, // todo in another issue + // relayManager: RelayManager // todo in another issue + ) => { + // injector.btns(); // todo in another issue - add compose button + this.replacer.runIntervalFunctionsPeriodically(); + await notifications.showInitial(acctEmail); + notifications.show( + 'FlowCrypt Thunderbird support is still in early development, and not expected to function properly yet. Support will be gradually added in upcoming versions.' + ); + }; +} diff --git a/extension/js/content_scripts/webmail/webmail.ts b/extension/js/content_scripts/webmail/webmail.ts index 5e5d6adf937..beadcc1047f 100644 --- a/extension/js/content_scripts/webmail/webmail.ts +++ b/extension/js/content_scripts/webmail/webmail.ts @@ -6,6 +6,7 @@ import { Catch } from '../../common/platform/catch.js'; import { GmailWebmailStartup } from './gmail/gmail-webmail-startup.js'; +import { ThunderbirdWebmailStartup } from './thunderbird/thunderbird-webmail-startup.js'; declare global { interface Window { @@ -16,5 +17,10 @@ declare global { Catch.try(async () => { // when we support more webmails, there will be if/else here to figure out which one to run - await new GmailWebmailStartup().asyncConstructor(); + const browserName = Catch.browser().name; + if (browserName === 'thunderbird') { + await new ThunderbirdWebmailStartup().asyncConstructor(); + } else { + await new GmailWebmailStartup().asyncConstructor(); + } })(); diff --git a/scripts/build.sh b/scripts/build.sh index 8415f1d7fff..3e34cbd493b 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -81,12 +81,13 @@ main() { synchronize_files "build/chrome-enterprise" synchronize_files "build/chrome-consumer" synchronize_files "build/firefox-consumer" + synchronize_files "build/thunderbird-consumer" } exit 0 fi if [[ "$#" == 1 ]] && [[ "$1" == "--incremental" ]]; then - delete_directories $BUILD_DIRECTORY/firefox-consumer $BUILD_DIRECTORY/chrome-consumer $BUILD_DIRECTORY/chrome-consumer-mock $BUILD_DIRECTORY/chrome-enterprise $BUILD_DIRECTORY/chrome-enterprise-mock $BUILD_DIRECTORY/generic-extension-wip/js/content_scripts + delete_directories $BUILD_DIRECTORY/firefox-consumer $BUILD_DIRECTORY/thunderbird-consumer $BUILD_DIRECTORY/chrome-consumer $BUILD_DIRECTORY/chrome-consumer-mock $BUILD_DIRECTORY/chrome-enterprise $BUILD_DIRECTORY/chrome-enterprise-mock $BUILD_DIRECTORY/generic-extension-wip/js/content_scripts # build concurrently - using standard typescript compiler with --incremental flag mkdir -p $BUILD_DIRECTORY build_typescript_project false "./tsconfig.json" true "$BUILD_DIRECTORY/tsconfig.tsbuildinfo" @@ -150,6 +151,7 @@ main() { cp -r $OUTPUT_DIRECTORY ./build/chrome-enterprise cp -r $OUTPUT_DIRECTORY ./build/chrome-consumer cp -r $OUTPUT_DIRECTORY ./build/firefox-consumer + cp -r $OUTPUT_DIRECTORY ./build/thunderbird-consumer node ./build/tooling/build-types-and-manifests } diff --git a/test/source/patterns.ts b/test/source/patterns.ts index 48de01d0a31..cb8a4ed9459 100644 --- a/test/source/patterns.ts +++ b/test/source/patterns.ts @@ -94,7 +94,7 @@ for (const buildType of ['chrome-consumer', 'chrome-enterprise']) { } for (const expectedPermission of expectedPermissions) { if (!manifest.permissions.includes(expectedPermission)) { - if (!(expectedPermission === 'unlimitedStorage' && buildType === 'firefox-consumer')) { + if (!(expectedPermission === 'unlimitedStorage' && (buildType === 'firefox-consumer' || buildType === 'thunderbird-consumer'))) { console.error(`Missing permission '${expectedPermission}' in ${buildType}/manifest.json`); errsFound++; } diff --git a/tooling/build-types-and-manifests.ts b/tooling/build-types-and-manifests.ts index 807bcd6965a..d613e94efe1 100644 --- a/tooling/build-types-and-manifests.ts +++ b/tooling/build-types-and-manifests.ts @@ -15,8 +15,8 @@ const DIR = './build'; const version: string = JSON.parse(readFileSync('./package.json').toString()).version; // eslint-disable-next-line @typescript-eslint/no-explicit-any -const addManifest = (toBuildType: string, transform: (manifest: { [k: string]: any }) => void) => { - const manifest = JSON.parse(readFileSync(`${DIR}/generic-extension-wip/manifest.json`).toString()); +const addManifest = (toBuildType: string, transform: (manifest: { [k: string]: any }) => void, fromBuildType = 'generic-extension-wip') => { + const manifest = JSON.parse(readFileSync(`${DIR}/${fromBuildType}/manifest.json`).toString()); transform(manifest); writeFileSync(`${DIR}/${toBuildType}/manifest.json`, JSON.stringify(manifest, undefined, 2)); }; @@ -49,6 +49,29 @@ addManifest('firefox-consumer', manifest => { delete manifest.minimum_chrome_version; }); +addManifest( + 'thunderbird-consumer', + manifest => { + manifest.browser_specific_settings.strict_min_version = '102.0'; + manifest.browser_action.default_title = 'FlowCrypt Encryption for Thunderbird'; + manifest.name = 'FlowCrypt Encryption for Thunderbird'; + manifest.description = 'Secure end-to-end encryption with FlowCrypt'; // needs to updated later + manifest.compose_action = { + default_title: 'FlowCrypt', // eslint-disable-line @typescript-eslint/naming-convention + default_icon: '/img/logo/flowcrypt-logo-64-64.png', // eslint-disable-line @typescript-eslint/naming-convention + // default_popup will be updated later + default_popup: '/chrome/popups/default.htm', // eslint-disable-line @typescript-eslint/naming-convention + }; + manifest.message_display_action = { + default_title: 'FlowCrypt', // eslint-disable-line @typescript-eslint/naming-convention + default_icon: '/img/logo/flowcrypt-logo-64-64.png', // eslint-disable-line @typescript-eslint/naming-convention + // default_popup will be updated later + default_popup: '/chrome/popups/default.htm', // eslint-disable-line @typescript-eslint/naming-convention + }; + }, + 'firefox-consumer' +); + addManifest('chrome-enterprise', manifest => { manifest.version = version; manifest.name = 'FlowCrypt for Enterprise';