From 1f28a20a8d315253bfbea85d9010e37bbdb9a45f Mon Sep 17 00:00:00 2001 From: Brendan Meyer Date: Wed, 12 Jul 2023 12:46:42 +1000 Subject: [PATCH 1/2] mailto links work with single recpients per to,cc,bcc - added mailto handle registry settings for Windows and dialog selection --- package-lock.json | 17 ++- package.json | 1 + src/main/account-views/preload/gmail.ts | 175 ++++++++++++++++++++---- src/main/app.ts | 10 +- src/main/menus/app.ts | 84 +++++++++++- 5 files changed, 250 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index e331ec5d..24bbe2a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "gmail-desktop", - "version": "3.0.0-alpha.35", + "version": "3.0.0-alpha.37", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gmail-desktop", - "version": "3.0.0-alpha.35", + "version": "3.0.0-alpha.37", "hasInstallScript": true, "license": "MIT", "dependencies": { - "@sindresorhus/do-not-disturb": "^1.1.0" + "@sindresorhus/do-not-disturb": "^1.1.0", + "winreg": "^1.2.4" }, "devDependencies": { "@babel/core": "^7.18.10", @@ -29461,6 +29462,11 @@ "node": ">=8" } }, + "node_modules/winreg": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.4.tgz", + "integrity": "sha512-IHpzORub7kYlb8A43Iig3reOvlcBJGX9gZ0WycHhghHtA65X0LYnMRuJs+aH1abVnMJztQkvQNlltnbPi5aGIA==" + }, "node_modules/word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -52607,6 +52613,11 @@ "string-width": "^4.0.0" } }, + "winreg": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.4.tgz", + "integrity": "sha512-IHpzORub7kYlb8A43Iig3reOvlcBJGX9gZ0WycHhghHtA65X0LYnMRuJs+aH1abVnMJztQkvQNlltnbPi5aGIA==" + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", diff --git a/package.json b/package.json index 69719e88..78732f80 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "stylelint-config-xo": "^0.20.0", "type-fest": "^0.21.3", "typescript": "^4.2.3", + "winreg": "^1.2.4", "write-json-file": "^4.3.0", "xo": "^0.38.2" }, diff --git a/src/main/account-views/preload/gmail.ts b/src/main/account-views/preload/gmail.ts index 6b3d9c5f..cf2266a8 100644 --- a/src/main/account-views/preload/gmail.ts +++ b/src/main/account-views/preload/gmail.ts @@ -253,42 +253,161 @@ export function initGmail() { } ) - ipcRenderer.on('gmail:compose-mail', async (_event, to?: string) => { - clickElement('div[gh="cm"]') + ipcRenderer.on( + 'gmail:compose-mail', + async (_event, mailtoString?: string) => { + if (!mailtoString) { + return + } - if (!to) { - return - } + // Decode mailto string + const url = new URL(mailtoString) - const toElement = await elementReady( - 'textarea[name="to"]', - { - stopOnDomReady: false, - timeout: 60000 + const to = decodeURIComponent(url.pathname) + const parameters = new URLSearchParams(url.search) + + const cc = parameters.get('cc') + const bcc = parameters.get('bcc') + const subject = parameters.get('subject') + const body = parameters.get('body') + + if (!to) { + return } - ) - if (!toElement) { - return - } + clickElement('div[gh="cm"]') - toElement.focus() - toElement.value = to + const newMessageElement = await elementReady( + 'div[aria-label="New Message"]:not([aria-checked])', + { + stopOnDomReady: false, + timeout: 60000 + } + ) - const subjectElement = document.querySelector( - 'input[name="subjectbox"]' - ) + if (!newMessageElement) { + return + } - if (!subjectElement) { - return - } + newMessageElement.ariaChecked = 'true' - // The subject input can't be focused immediately after - // settings the "to" input value for an unknown reason. - setTimeout(() => { - subjectElement.focus() - }, 200) - }) + const toDivElement = newMessageElement.querySelector( + 'div[name="to"]' + ) + + if (!toDivElement) { + return + } + + toDivElement.focus() + await new Promise((resolve) => { + setTimeout(resolve, 200) + }) + + const toElement = toDivElement.querySelector('input') + if (!toElement) { + return + } + + toElement.focus() + toElement.value = to + + if (cc) { + const ccButtonElement = await elementReady( + 'span[class="aB gQ pE"]', + { + target: newMessageElement, + stopOnDomReady: false, + timeout: 60000 + } + ) + if (!ccButtonElement) { + return + } + + ccButtonElement.click() + + const ccElement = await elementReady( + 'div[name="cc"] input', + { + target: newMessageElement, + stopOnDomReady: false, + timeout: 60000 + } + ) + if (!ccElement) { + return + } + + ccElement.focus() + ccElement.value = cc + } + + if (bcc) { + const bccButtonElement = await elementReady( + 'span[class="aB gQ pB"]', + { + target: newMessageElement, + stopOnDomReady: false, + timeout: 60000 + } + ) + if (!bccButtonElement) { + return + } + + bccButtonElement.click() + + const bccElement = await elementReady( + 'div[name="bcc"] input', + { + target: newMessageElement, + stopOnDomReady: false, + timeout: 60000 + } + ) + if (!bccElement) { + return + } + + bccElement.focus() + bccElement.value = bcc + } + + const subjectElement = newMessageElement.querySelector( + 'input[name="subjectbox"]' + ) + if (!subjectElement) { + return + } + + let subjectSet = false + + // The subject input can't be focused immediately after + // settings the "to" input value for an unknown reason. + setTimeout(() => { + subjectElement.focus() + if (subject) { + subjectElement.value = subject + subjectSet = true + } + }, 200) + + if (body || subjectSet) { + const bodyElement = newMessageElement.querySelector( + 'div[role="textbox"]' + ) + if (!bodyElement) { + return + } + + if (body) { + bodyElement.focus() + bodyElement.textContent = body + } + } + } + ) setInterval(() => { previousNewMails.clear() diff --git a/src/main/app.ts b/src/main/app.ts index 537c0638..e320a524 100644 --- a/src/main/app.ts +++ b/src/main/app.ts @@ -35,12 +35,18 @@ export async function initApp() { app.disableHardwareAcceleration() } - app.on('second-instance', () => { + app.on('second-instance', (_event, argv, _workingDirectory) => { + const mailtoString = argv.find((s) => s.startsWith('mailto:')) + + if (mailtoString) { + sendToSelectedAccountView('gmail:compose-mail', mailtoString) + } + showMainWindow() }) app.on('open-url', (_event, mailto) => { - sendToSelectedAccountView('gmail:compose-mail', mailto.split(':')[1]) + sendToSelectedAccountView('gmail:compose-mail', mailto) showMainWindow() }) diff --git a/src/main/menus/app.ts b/src/main/menus/app.ts index f17dc272..e829d9a0 100644 --- a/src/main/menus/app.ts +++ b/src/main/menus/app.ts @@ -11,6 +11,7 @@ import { } from 'electron' import * as fs from 'fs' import { is } from 'electron-util' +import Registry from 'winreg' import { checkForUpdatesWithFeedback, changeReleaseChannel, @@ -41,7 +42,7 @@ import { hideAccountViews, sendToSelectedAccountView } from '../account-views' -import { gitHubRepoUrl, gmailUrl } from '../../constants' +import { appId, gitHubRepoUrl, gmailUrl } from '../../constants' import { openExternalUrl } from '../utils/url' interface AppearanceMenuItem { @@ -289,11 +290,86 @@ export function getAppMenu() { type: 'separator' }, { - label: 'Default Mail Client', - type: 'checkbox', + label: is.windows + ? 'Set as Default Mail Client' + : 'Default Mail Client', + type: is.windows ? 'normal' : 'checkbox', checked: app.isDefaultProtocolClient('mailto'), click({ checked }) { - if (checked) { + if (is.windows) { + const exePath = app.getPath('exe') + + const registryData = new Map() + registryData.set( + `\\Software\\Classes\\${appId}.Mailto\\`, + 'URL:mailto' + ) + registryData.set( + `\\Software\\Classes\\${appId}.Mailto\\URL Protocol`, + '' + ) + registryData.set( + `\\Software\\Classes\\${appId}.Mailto\\shell\\open\\command\\`, + `"${exePath}" %1` + ) + registryData.set( + `\\Software\\RegisteredApplications\\Gmail-Desktop`, + `SOFTWARE\\${appId}\\Capabilities` + ) + registryData.set( + `\\Software\\${appId}\\Capabilities\\ApplicationDescription`, + `Open mailto in Gmail Desktop` + ) + registryData.set( + `\\Software\\${appId}\\Capabilities\\UrlAssociations\\mailto`, + `${appId}.Mailto` + ) + registryData.set( + `\\Software\\Wow6432Node\\RegisteredApplications\\Gmail-Desktop`, + `SOFTWARE\\${appId}\\Capabilities` + ) + registryData.set( + `\\Software\\Wow6432Node\\${appId}\\Capabilities\\ApplicationDescription`, + `Open mailto in Gmail Desktop` + ) + registryData.set( + `\\Software\\Wow6432Node\\${appId}\\Capabilities\\UrlAssociations\\mailto`, + `${appId}.Mailto` + ) + + for (const [key, value] of registryData) { + const parts = key.split('\\') + const name = parts.pop() + const keyPath = parts.join('\\') + + const regKey = new Registry({ + hive: Registry.HKCU, + key: keyPath + }) + + regKey.set(name, Registry.REG_SZ, value, (error) => { + if (error) { + dialog.showMessageBox({ + type: 'error', + message: `Error setting as default mail client. ${JSON.stringify( + error + )}` + }) + } + }) + } + + app.setAsDefaultProtocolClient('mailto') + + dialog.showMessageBoxSync(getMainWindow(), { + type: 'info', + message: `The Windows Default Applications settings menu will now open, please wait for it to load, and change the Default MAILTO handler to ${app.name}.` + }) + + shell.openExternal( + `ms-settings:defaultapps?registeredAppUser=Gmail-Desktop` + ) + } else if (checked) { const isSetMailClient = app.setAsDefaultProtocolClient( 'mailto' ) From 66eb38687d8579e5ce5e3a747e88434f6397d9f3 Mon Sep 17 00:00:00 2001 From: Brendan Meyer Date: Wed, 12 Jul 2023 12:56:16 +1000 Subject: [PATCH 2/2] add winreg types --- package-lock.json | 23 +++++++++++++++++++---- package.json | 1 + 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 24bbe2a4..d2afea29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@sindresorhus/do-not-disturb": "^1.1.0", - "winreg": "^1.2.4" + "@sindresorhus/do-not-disturb": "^1.1.0" }, "devDependencies": { "@babel/core": "^7.18.10", @@ -30,6 +29,7 @@ "@types/lodash.debounce": "^4.0.6", "@types/node": "^14.14.34", "@types/react-dom": "^18.0.6", + "@types/winreg": "^1.2.32", "babel-loader": "^8.2.5", "builder-util-runtime": "^8.7.3", "cross-env": "^7.0.3", @@ -63,6 +63,7 @@ "stylelint-config-xo": "^0.20.0", "type-fest": "^0.21.3", "typescript": "^4.2.3", + "winreg": "^1.2.4", "write-json-file": "^4.3.0", "xo": "^0.38.2" } @@ -8534,6 +8535,12 @@ "node": ">=0.10.0" } }, + "node_modules/@types/winreg": { + "version": "1.2.32", + "resolved": "https://registry.npmjs.org/@types/winreg/-/winreg-1.2.32.tgz", + "integrity": "sha512-+TgocDdajdQVGVDw0XxaOH2rn2YTb8zHqDKdcqj9bHiE0NZaU1KL9StdApF74bNvMx+itYabv9NT6E/iB94N/A==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "4.16.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.16.1.tgz", @@ -29465,7 +29472,8 @@ "node_modules/winreg": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.4.tgz", - "integrity": "sha512-IHpzORub7kYlb8A43Iig3reOvlcBJGX9gZ0WycHhghHtA65X0LYnMRuJs+aH1abVnMJztQkvQNlltnbPi5aGIA==" + "integrity": "sha512-IHpzORub7kYlb8A43Iig3reOvlcBJGX9gZ0WycHhghHtA65X0LYnMRuJs+aH1abVnMJztQkvQNlltnbPi5aGIA==", + "dev": true }, "node_modules/word-wrap": { "version": "1.2.3", @@ -36296,6 +36304,12 @@ } } }, + "@types/winreg": { + "version": "1.2.32", + "resolved": "https://registry.npmjs.org/@types/winreg/-/winreg-1.2.32.tgz", + "integrity": "sha512-+TgocDdajdQVGVDw0XxaOH2rn2YTb8zHqDKdcqj9bHiE0NZaU1KL9StdApF74bNvMx+itYabv9NT6E/iB94N/A==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "4.16.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.16.1.tgz", @@ -52616,7 +52630,8 @@ "winreg": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.4.tgz", - "integrity": "sha512-IHpzORub7kYlb8A43Iig3reOvlcBJGX9gZ0WycHhghHtA65X0LYnMRuJs+aH1abVnMJztQkvQNlltnbPi5aGIA==" + "integrity": "sha512-IHpzORub7kYlb8A43Iig3reOvlcBJGX9gZ0WycHhghHtA65X0LYnMRuJs+aH1abVnMJztQkvQNlltnbPi5aGIA==", + "dev": true }, "word-wrap": { "version": "1.2.3", diff --git a/package.json b/package.json index 78732f80..4c0a0908 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@types/lodash.debounce": "^4.0.6", "@types/node": "^14.14.34", "@types/react-dom": "^18.0.6", + "@types/winreg": "^1.2.32", "babel-loader": "^8.2.5", "builder-util-runtime": "^8.7.3", "cross-env": "^7.0.3",