forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgmailSession.ts
More file actions
212 lines (191 loc) · 8.25 KB
/
Copy pathgmailSession.ts
File metadata and controls
212 lines (191 loc) · 8.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
// Gmail "session" connector (Option B) — Electron-coupled glue (main process).
//
// Windows can't harvest the system browser's Google cookies the way macOS does
// (Chrome 127+ App-Bound Encryption makes that an infostealer technique). Instead we
// own the jar: the user signs into Google ONCE inside an Omi-owned BrowserWindow on a
// PERSISTENT session partition, and we replay the same Gmail web endpoints macOS uses
// over that session (cookies auto-attach). No restricted-scope OAuth, no DPAPI.
//
// This module holds everything that touches Electron (BrowserWindow / session / net);
// the fetch/parse cascade lives in gmailSessionReader.ts + gmailSessionParse.ts and is
// unit-tested in isolation. Never log cookies or email contents here (repo PII rules).
import { BrowserWindow, net, session, type Session } from 'electron'
import {
readRecentEmails,
verifyConnection,
type GmailHttpResponse,
type GmailReaderDeps
} from './gmailSessionReader'
import { hasGoogleAuthCookies, buildGmailLoginUrl } from './gmailSessionParse'
import { installContextMenu } from '../contextMenu'
import type { GmailSessionStatus, GmailSessionFetchResult } from '../../shared/types'
// Persistent partition: cookies survive restarts, stored in Chromium's own encrypted
// cookie store under userData — we never touch DPAPI or another app's profile.
const PARTITION = 'persist:omi-gmail'
// A genuine desktop Chrome UA. Google blocks logins from "embedded" user agents
// (disallowed_useragent), so both the login window and the feed requests present as
// desktop Chrome on Windows — this is the Option B mitigation.
const CHROME_UA =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
// The sign-in URL is built per-call by buildGmailLoginUrl (adds login_hint when the
// signed-in Omi email is known) — see gmailSessionParse.ts.
const LOGIN_TIMEOUT_MS = 5 * 60_000
const HTTP_TIMEOUT_MS = 30_000
function getGmailSession(): Session {
return session.fromPartition(PARTITION)
}
/** Cookie names applicable to Gmail in our partition (used to detect a signed-in session). */
async function getAuthCookieNames(): Promise<string[]> {
try {
const ses = getGmailSession()
const cookies = await ses.cookies.get({ url: 'https://mail.google.com/' })
return cookies.map((c) => c.name)
} catch {
return []
}
}
/** GET a URL over the Gmail partition session (cookies auto-attach). Never throws. */
function httpGet(url: string): Promise<GmailHttpResponse> {
return new Promise((resolve) => {
let settled = false
const done = (r: GmailHttpResponse): void => {
if (settled) return
settled = true
clearTimeout(timer)
resolve(r)
}
let request: Electron.ClientRequest
try {
request = net.request({ method: 'GET', url, session: getGmailSession(), redirect: 'follow' })
} catch (e) {
resolve({ status: null, body: '', error: (e as Error).message })
return
}
request.setHeader('User-Agent', CHROME_UA)
const timer = setTimeout(() => {
try {
request.abort()
} catch {
/* already finished */
}
done({ status: null, body: '', error: 'timeout' })
}, HTTP_TIMEOUT_MS)
request.on('response', (response) => {
const chunks: Buffer[] = []
response.on('data', (chunk: Buffer) => chunks.push(chunk))
response.on('end', () =>
done({ status: response.statusCode, body: Buffer.concat(chunks).toString('utf8') })
)
response.on('error', (err: Error) => done({ status: null, body: '', error: err.message }))
})
request.on('error', (err: Error) => done({ status: null, body: '', error: err.message }))
request.end()
})
}
const readerDeps: GmailReaderDeps = { httpGet, getAuthCookieNames }
/**
* Open the Google login window on the persistent partition and resolve once the
* session is authenticated (auth cookies present) or the window is closed/times out.
* Verifying against Gmail happens separately via the reader.
*
* `email` is the signed-in Omi user's Google address (from the renderer); when present
* it is passed to Google as `login_hint` so the window lands on "Continue as <account>".
*/
export function gmailSessionConnect(email?: string): Promise<GmailSessionStatus> {
const ses = getGmailSession()
return new Promise((resolve) => {
let settled = false
// Parent it to the main window (not modal — the sign-in shouldn't block the app),
// and mirror billing/checkoutWindow.ts: fully isolated remote content, shown only
// once ready to avoid a white flash.
const parent = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0] ?? undefined
const win = new BrowserWindow({
width: 520,
height: 700,
parent,
show: false,
title: 'Connect Gmail',
autoHideMenuBar: true,
webPreferences: {
session: ses,
contextIsolation: true,
nodeIntegration: false,
sandbox: true
}
})
win.webContents.setUserAgent(CHROME_UA)
// Right-click → Paste, so the user can paste their email/password into Google's
// form. Chromium-role menu only; exposes nothing of the app to the remote page.
installContextMenu(win)
// Timers reference `settle` and vice-versa, so collect teardown in an array to
// keep both the interval and the timeout `const` (no forward-declared `let`).
const cleanups: Array<() => void> = []
const settle = (status: GmailSessionStatus): void => {
if (settled) return
settled = true
cleanups.forEach((fn) => fn())
resolve(status)
}
const finishConnected = async (): Promise<void> => {
if (settled) return
// Persist the freshly minted cookies before closing so the next launch is warm.
try {
await ses.cookies.flushStore()
} catch {
/* best-effort */
}
settle({ connected: true, verifiedAt: Date.now() })
if (!win.isDestroyed()) win.close()
}
const check = async (): Promise<void> => {
if (settled) return
if (hasGoogleAuthCookies(await getAuthCookieNames())) await finishConnected()
}
win.webContents.on('did-navigate', () => void check())
win.webContents.on('did-navigate-in-page', () => void check())
win.webContents.on('did-frame-navigate', () => void check())
win.on('closed', () => settle({ connected: false, message: 'Sign-in window was closed.' }))
const poll = setInterval(() => void check(), 1500)
cleanups.push(() => clearInterval(poll))
const timer = setTimeout(() => {
settle({ connected: false, message: 'Timed out waiting for Google sign-in.' })
if (!win.isDestroyed()) win.close()
}, LOGIN_TIMEOUT_MS)
cleanups.push(() => clearTimeout(timer))
win.once('ready-to-show', () => win.show())
void win.loadURL(buildGmailLoginUrl(email), { userAgent: CHROME_UA })
})
}
/** Lightweight status: signed in iff the partition holds Google auth cookies. */
export async function gmailSessionStatus(): Promise<GmailSessionStatus> {
const connected = hasGoogleAuthCookies(await getAuthCookieNames())
return connected
? { connected: true }
: { connected: false, message: 'Not connected. Click Connect to sign into Gmail.' }
}
/** Fetch recent emails over the persisted session, normalized to the macOS shape. */
export async function gmailSessionFetch(
query?: string,
maxResults?: number
): Promise<GmailSessionFetchResult> {
const out = await readRecentEmails(readerDeps, { query, maxResults })
if (out.ok) return { ok: true, emails: out.emails, source: out.source }
return { ok: false, emails: [], error: out.error }
}
/** Verify the session actually reads Gmail right now (network probe via the reader). */
export async function gmailSessionVerify(): Promise<GmailSessionStatus> {
return verifyConnection(readerDeps)
}
/** Disconnect: clear the partition (cookies + cached data), signing the session out. */
export async function gmailSessionDisconnect(): Promise<GmailSessionStatus> {
try {
const ses = getGmailSession()
await ses.clearStorageData({
storages: ['cookies', 'localstorage', 'indexdb', 'serviceworkers']
})
await ses.clearCache()
} catch {
/* best-effort — a partial clear still drops the auth cookies */
}
return { connected: false, message: 'Disconnected.' }
}