forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGmailAccountSelection.swift
More file actions
161 lines (144 loc) · 6.24 KB
/
Copy pathGmailAccountSelection.swift
File metadata and controls
161 lines (144 loc) · 6.24 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
import Foundation
struct GmailAccountOption: Identifiable, Equatable {
let id: String
let browserName: String
let email: String?
}
enum GmailSelectionStore {
private static let selectedPathKey = DefaultsKey.gmailSelectedCookiePath.rawValue
private static let selectedLabelKey = DefaultsKey.gmailSelectedAccountLabel.rawValue
static var selectedCookiePath: String? {
get { UserDefaults.standard.string(forKey: selectedPathKey) }
set { UserDefaults.standard.set(newValue, forKey: selectedPathKey) }
}
static var selectedAccountLabel: String {
UserDefaults.standard.string(forKey: selectedLabelKey) ?? ""
}
static var hasMadeChoice: Bool {
selectedCookiePath != nil || !selectedAccountLabel.isEmpty
}
static func persist(cookiePath: String?, label: String) {
selectedCookiePath = cookiePath
UserDefaults.standard.set(label, forKey: selectedLabelKey)
}
static func resetForTesting() {
UserDefaults.standard.removeObject(forKey: selectedPathKey)
UserDefaults.standard.removeObject(forKey: selectedLabelKey)
}
static func filter(_ configs: [[String: String]]) -> [[String: String]] {
filter(configs, selectedCookiePath: selectedCookiePath)
}
/// Filter with an explicit snapshot of the selected profile. Callers that
/// run several fetches for one logical read pass the snapshot captured at
/// read entry, so a picker change mid-read cannot mix two accounts' mail.
static func filter(_ configs: [[String: String]], selectedCookiePath: String?) -> [[String: String]] {
guard let selected = selectedCookiePath, !selected.isEmpty else { return configs }
let narrowed = configs.filter { $0["db_path"] == selected }
guard narrowed.isEmpty else { return narrowed }
// The stored selection no longer matches any configured profile (cookies
// deleted, profile removed, path changed). Widening back to every profile
// silently changes which inbox is read while Settings still shows the old
// label — record the fail-open so it is not invisible.
DesktopDiagnosticsManager.shared.recordFallback(
area: "gmail_account_selection",
from: "selected_account",
to: "first_readable_profile",
reason: "selected_profile_missing",
outcome: .recovered)
return configs
}
}
enum GmailAccountProbe {
static func availableAccounts() async throws -> [GmailAccountOption] {
let configs = BrowserGoogleSession.configsForPython(logPrefix: "GmailAccountProbe")
guard !configs.isEmpty else { return [] }
let configJSON: String
do {
let data = try JSONSerialization.data(withJSONObject: configs)
configJSON = String(data: data, encoding: .utf8) ?? "[]"
} catch {
throw error
}
let pythonScript = """
\(BrowserGoogleSession.chromiumCookiePythonSupport)
import re
from urllib.request import Request, build_opener, HTTPCookieProcessor
browsers = json.loads(sys.stdin.read())
accounts = []
for browser in browsers:
cookies, err = decrypt_google_cookies(browser['db_path'], browser['password'], include_gmail_hosts=True)
if err or not cookies:
continue
if not [c for c in cookies if c['name'] in GOOGLE_AUTH_COOKIE_NAMES]:
continue
jar = make_cookie_jar(cookies)
email = None
for url in ('https://accounts.google.com/AccountInfo', 'https://www.google.com/accounts/AccountInfo'):
try:
opener = build_opener(HTTPCookieProcessor(jar))
req = Request(url)
req.add_header('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36')
resp = opener.open(req, timeout=15)
body = resp.read().decode('utf-8', errors='replace')
match = re.search(r'<email>([^<]+)</email>', body)
if match:
email = match.group(1)
break
except Exception:
continue
accounts.append({'name': browser['name'], 'db_path': browser['db_path'], 'email': email})
write_json_result('omi_gmail_accounts_', {'ok': True, 'accounts': accounts})
"""
do {
let result = try BrowserPythonRunner.run(
script: pythonScript,
arguments: [],
stdinData: Data(configJSON.utf8),
// Two sequential AccountInfo probes per profile with a 15s request
// timeout; give the process budget for every configured profile so a
// slow Google endpoint cannot zero out all discovered accounts.
timeoutSeconds: 45 + 30 * max(configs.count, 1)
)
let outputPath =
String(data: result.stdout, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !outputPath.isEmpty, FileManager.default.fileExists(atPath: outputPath) else {
throw GmailAccountProbeError.noOutput
}
defer { try? FileManager.default.removeItem(atPath: outputPath) }
let data = try Data(contentsOf: URL(fileURLWithPath: outputPath))
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw GmailAccountProbeError.invalidOutput
}
return Self.parseAccounts(json)
}
}
enum GmailAccountProbeError: LocalizedError {
case noOutput
case invalidOutput
var errorDescription: String? {
switch self {
case .noOutput:
return "Gmail account probe produced no output."
case .invalidOutput:
return "Gmail account probe returned unreadable output."
}
}
}
static func parseAccounts(_ json: [String: Any]) -> [GmailAccountOption] {
let raw = (json["accounts"] as? [[String: Any]] ?? []).sorted { lhs, rhs in
let lhsName = lhs["name"] as? String ?? ""
let rhsName = rhs["name"] as? String ?? ""
return lhsName.localizedStandardCompare(rhsName) == .orderedAscending
}
return raw.compactMap { dict in
guard let path = dict["db_path"] as? String, !path.isEmpty else { return nil }
let email = (dict["email"] as? String).flatMap { $0.isEmpty ? nil : $0 }
return GmailAccountOption(
id: path,
browserName: dict["name"] as? String ?? "Browser",
email: email
)
}
}
}