diff --git a/src/main/account-views/preload/gmail.ts b/src/main/account-views/preload/gmail.ts
index 2d1b419b..30329b74 100644
--- a/src/main/account-views/preload/gmail.ts
+++ b/src/main/account-views/preload/gmail.ts
@@ -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() {
@@ -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()
diff --git a/src/main/app.ts b/src/main/app.ts
index 7ffa23b5..b7a00f22 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 } from './main-window'
-import { sendToSelectedAccountView } from './account-views'
 import { appId } from '../constants'
 
 let isQuittingApp = false
@@ -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', () => {
diff --git a/src/main/gmail.ts b/src/main/gmail.ts
index 8abc432c..2c2adadd 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 { 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 = {}
 
@@ -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<T extends new (...args: any) => Error>(
+  value: unknown,
+  errorType: T
+): value is InstanceType<T> & NodeJS.ErrnoException {
+  return value instanceof errorType
+}