From bf778913d3d66b2ff97f6cb5c02754e12e3bcaf9 Mon Sep 17 00:00:00 2001 From: Alec Mev Date: Wed, 23 Jun 2021 17:26:18 +0100 Subject: [PATCH] Handle mailto in argv, support full syntax --- src/main/account-views/preload/gmail.ts | 143 +++++++++++++++++------- src/main/app.ts | 8 +- src/main/gmail.ts | 12 +- src/main/mailto.ts | 33 ++++++ src/utils/type-helpers.ts | 7 ++ 5 files changed, 161 insertions(+), 42 deletions(-) create mode 100644 src/main/mailto.ts create mode 100644 src/utils/type-helpers.ts diff --git a/src/main/account-views/preload/gmail.ts b/src/main/account-views/preload/gmail.ts index 1e1271c8..ec1a2b1a 100644 --- a/src/main/account-views/preload/gmail.ts +++ b/src/main/account-views/preload/gmail.ts @@ -198,11 +198,109 @@ function parseNewMails(feedDocument: Document) { return newMails } -function clickElement(selector: string) { - const element = document.querySelector(selector) - if (element) { - element.click() +const nextTick = async () => + new Promise((resolve) => { + setTimeout(resolve, 0) + }) + +async function composeMail( + _event: IpcRendererEvent, + to?: string, + cc?: string | null, + bcc?: string | null, + subject?: string | null, + body?: string | null +) { + // Can't use element ids as selectors since they aren't stable + + const composeButton = await elementReady('div[gh="cm"]', { + stopOnDomReady: false, + timeout: 60000 + }) + if (!composeButton) throw new Error('No composeButton') + composeButton.click() + + // Out of the "to" block because we use the readiness of this element to + // know when we can start filling out the fields + const toElement = await elementReady( + 'textarea[name="to"]', + { + stopOnDomReady: false, + timeout: 60000 + } + ) + const ccElement = document.querySelector( + 'textarea[name="cc"]' + ) + const bccElement = document.querySelector( + 'textarea[name="bcc"]' + ) + const subjectElement = document.querySelector( + 'input[name="subjectbox"]' + ) + const bodyElement = document.querySelector( + 'div[aria-label="Message Body"]' + ) + + if (!toElement) throw new Error('No toElement') + if (!ccElement) throw new Error('No ccElement') + if (!bccElement) throw new Error('No bccElement') + if (!subjectElement) throw new Error('No subjectElement') + if (!bodyElement) throw new Error('No bodyElement') + + if (to) { + toElement.focus() + toElement.value = to + await nextTick() + } + + // Why the nextTick at the end of each block? Because otherwise the + // fields that follow may fail to get focused for some reason. This + // isn't the original comment, see prior commits. + + if (cc) { + document + .querySelector( + 'span[aria-label="Add Cc recipients ‪(Ctrl-Shift-C)‬"]' + ) + ?.click() + ccElement.focus() + ccElement.value = cc + await nextTick() + } + + if (bcc) { + document + .querySelector( + 'span[aria-label="Add Bcc recipients ‪(Ctrl-Shift-B)‬"]' + ) + ?.click() + bccElement.focus() + bccElement.value = bcc + await nextTick() } + + if (subject) { + subjectElement.focus() + subjectElement.value = subject + await nextTick() + } + + if (body) { + bodyElement.focus() + bodyElement.innerHTML = body + await nextTick() + } + + /* eslint-disable no-negated-condition */ + if (!to) { + toElement.focus() + } else if (!subject) { + subjectElement.focus() + } else { + bodyElement.focus() + } + /* eslint-enable no-negated-condition */ } export function initGmail() { @@ -253,42 +351,9 @@ export function initGmail() { } ) - ipcRenderer.on('gmail:compose-mail', async (_event, to?: string) => { - clickElement('div[gh="cm"]') - - if (!to) { - return - } - - const toElement = await elementReady( - 'textarea[name="to"]', - { - stopOnDomReady: false, - timeout: 60000 - } - ) - - if (!toElement) { - return - } - - toElement.focus() - toElement.value = to + ipcRenderer.on('gmail:compose-mail', composeMail) - const subjectElement = document.querySelector( - 'input[name="subjectbox"]' - ) - - if (!subjectElement) { - return - } - - // The subject input can't be focused immediately after - // settings the "to" input value for an unknown reason. - setTimeout(() => { - subjectElement.focus() - }, 200) - }) + ipcRenderer.send('gmail:ready') setInterval(() => { previousNewMails.clear() diff --git a/src/main/app.ts b/src/main/app.ts index 537c0638..3a46bdda 100644 --- a/src/main/app.ts +++ b/src/main/app.ts @@ -1,7 +1,7 @@ import { app } from 'electron' import config, { ConfigKey } from './config' +import { handleMailto } from './mailto' import { getMainWindow, showMainWindow } from './main-window' -import { sendToSelectedAccountView } from './account-views' import { appId } from '../constants' let isQuittingApp = false @@ -36,11 +36,15 @@ export async function initApp() { } app.on('second-instance', () => { + if (getMainWindow()) { + handleMailto(argv[argv.length - 1]) + } + showMainWindow() }) app.on('open-url', (_event, mailto) => { - sendToSelectedAccountView('gmail:compose-mail', mailto.split(':')[1]) + handleMailto(mailto) showMainWindow() }) diff --git a/src/main/gmail.ts b/src/main/gmail.ts index 56a857ef..c9dabc2f 100644 --- a/src/main/gmail.ts +++ b/src/main/gmail.ts @@ -1,12 +1,13 @@ import { app, ipcMain, Notification } from 'electron' import { getAccountIdByViewId } from './account-views' -import { getAccount, selectAccount } from './accounts' +import { getAccount, selectAccount, getSelectedAccount } from './accounts' import { sendToMainWindow, showMainWindow } from './main-window' import config, { ConfigKey } from './config' import { is } from 'electron-util' import { updateTrayUnreadStatus } from './tray' import { Mail, UnreadCounts } from '../types' import { isEnabled as isDoNotDisturbEnabled } from '@sindresorhus/do-not-disturb' +import { handleMailto } from './mailto' const unreadCounts: UnreadCounts = {} @@ -134,4 +135,13 @@ export function handleGmail() { } }) } + + let isInitialMailtoHandled = false + ipcMain.on('gmail:ready', ({ sender }) => { + if (isInitialMailtoHandled) return + const accountId = getAccountIdByViewId(sender.id) + if (!accountId || accountId !== getSelectedAccount()?.id) return + isInitialMailtoHandled = true + handleMailto(process.argv[process.argv.length - 1]) + }) } diff --git a/src/main/mailto.ts b/src/main/mailto.ts new file mode 100644 index 00000000..9b751e5a --- /dev/null +++ b/src/main/mailto.ts @@ -0,0 +1,33 @@ +import { sendToSelectedAccountView } from './account-views' +import { instanceOfNodeError } from '../utils/type-helpers' + +function parse(uri: string) { + try { + return new URL(uri) + } catch (error: unknown) { + if ( + instanceOfNodeError(error, TypeError) && + error.code === 'ERR_INVALID_URL' + ) { + return + } + + throw error + } +} + +const spaceAfterComma = (x: string | null) => x?.replaceAll(',', ', ') + +export function handleMailto(uri?: string) { + if (!uri) return // Empty string doesn't cut it either + const x = parse(uri) + if (!x) return + sendToSelectedAccountView( + 'gmail:compose-mail', + spaceAfterComma(x.pathname), + spaceAfterComma(x.searchParams.get('cc')), + spaceAfterComma(x.searchParams.get('bcc')), + x.searchParams.get('subject'), + x.searchParams.get('body') + ) +} diff --git a/src/utils/type-helpers.ts b/src/utils/type-helpers.ts new file mode 100644 index 00000000..767571c2 --- /dev/null +++ b/src/utils/type-helpers.ts @@ -0,0 +1,7 @@ +// https://dev.to/jdbar/the-problem-with-handling-node-js-errors-in-typescript-and-the-workaround-m64 +export function instanceOfNodeError Error>( + value: unknown, + errorType: T +): value is InstanceType & NodeJS.ErrnoException { + return value instanceof errorType +}