Skip to content

Commit

Permalink
Handle mailto in argv, support full syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
alecmev committed Jul 16, 2021
1 parent 041d8ca commit ad85e6b
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 44 deletions.
143 changes: 104 additions & 39 deletions src/main/account-views/preload/gmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,109 @@ function parseNewMails(feedDocument: Document) {
return newMails
}

function clickElement(selector: string) {
const element = document.querySelector<HTMLDivElement>(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<HTMLDivElement>('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<HTMLTextAreaElement>(
'textarea[name="to"]',
{
stopOnDomReady: false,
timeout: 60000
}
)
const ccElement = document.querySelector<HTMLTextAreaElement>(
'textarea[name="cc"]'
)
const bccElement = document.querySelector<HTMLTextAreaElement>(
'textarea[name="bcc"]'
)
const subjectElement = document.querySelector<HTMLInputElement>(
'input[name="subjectbox"]'
)
const bodyElement = document.querySelector<HTMLDivElement>(
'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<HTMLSpanElement>(
'span[aria-label="Add Cc recipients ‪(Ctrl-Shift-C)‬"]'
)
?.click()
ccElement.focus()
ccElement.value = cc
await nextTick()
}

if (bcc) {
document
.querySelector<HTMLSpanElement>(
'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() {
Expand Down Expand Up @@ -254,42 +352,9 @@ export function initGmail() {
}
)

ipcRenderer.on('gmail:compose-mail', async (_event, to?: string) => {
clickElement('div[gh="cm"]')
ipcRenderer.on('gmail:compose-mail', composeMail)

if (!to) {
return
}

const toElement = await elementReady<HTMLTextAreaElement>(
'textarea[name="to"]',
{
stopOnDomReady: false,
timeout: 60000
}
)

if (!toElement) {
return
}

toElement.focus()
toElement.value = to

const subjectElement = document.querySelector<HTMLInputElement>(
'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()
Expand Down
8 changes: 4 additions & 4 deletions src/main/app.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { app } from 'electron'
import config, { ConfigKey } from './config'
import { handleMailto } from './mailto'
import { getMainWindow } from './main-window'
import { sendToSelectedAccountView } from './account-views'
import { appId } from '../constants'

let isQuittingApp = false
Expand Down Expand Up @@ -35,20 +35,20 @@ export async function initApp() {
app.disableHardwareAcceleration()
}

app.on('second-instance', () => {
app.on('second-instance', (_, argv) => {
const mainWindow = getMainWindow()

if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore()
}

mainWindow.show()
handleMailto(argv[argv.length - 1])
}
})

app.on('open-url', (_event, mailto) => {
sendToSelectedAccountView('gmail:compose-mail', mailto.split(':')[1])
handleMailto(mailto)
})

app.on('activate', () => {
Expand Down
12 changes: 11 additions & 1 deletion src/main/gmail.ts
Original file line number Diff line number Diff line change
@@ -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 { getMainWindow, sendToMainWindow } 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 = {}

Expand Down Expand Up @@ -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])
})
}
33 changes: 33 additions & 0 deletions src/main/mailto.ts
Original file line number Diff line number Diff line change
@@ -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')
)
}
7 changes: 7 additions & 0 deletions src/utils/type-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// https://dev.to/jdbar/the-problem-with-handling-node-js-errors-in-typescript-and-the-workaround-m64
export function instanceOfNodeError<T extends new (...args: any) => Error>(
value: unknown,
errorType: T
): value is InstanceType<T> & NodeJS.ErrnoException {
return value instanceof errorType
}

0 comments on commit ad85e6b

Please sign in to comment.