-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackground.js
More file actions
257 lines (221 loc) · 8.4 KB
/
background.js
File metadata and controls
257 lines (221 loc) · 8.4 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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
/**
* Thunderbird extension to open emails in Gmail web interface
*/
/**
* Cache of accountIds confirmed to be Gmail (consumer or Workspace).
* Negative results are not cached: Sent/Drafts messages may lack Gmail-specific headers,
* so we re-check until we see a received message that confirms the account is Gmail.
*/
const gmailAccountCache = new Set();
/**
* Check if an email address is a consumer Gmail/Googlemail account
* @param {string} email
* @returns {boolean}
*/
function isGmailEmailDomain(email) {
if (!email) return false;
const domain = email.split('@')[1]?.toLowerCase();
return domain === 'gmail.com' || domain === 'googlemail.com';
}
/**
* Check if a full message was delivered by Gmail's mail servers.
* Gmail adds "Authentication-Results: mx.google.com" to every delivered message,
* including Google Workspace accounts with custom domains.
* @param {Object} fullMessage - Full message object with headers
* @returns {boolean}
*/
function hasGmailHeaders(fullMessage) {
// Received messages: Gmail's MX server adds Authentication-Results
const authResults = fullMessage.headers['authentication-results'] || [];
const authValues = Array.isArray(authResults) ? authResults : [authResults];
if (authValues.some(v => v.includes('mx.google.com'))) return true;
// Sent messages: Gmail signs outgoing messages with X-Google-DKIM-Signature
const xGoogleDkim = fullMessage.headers['x-google-dkim-signature'];
if (Array.isArray(xGoogleDkim) ? xGoogleDkim.length > 0 : !!xGoogleDkim) return true;
return false;
}
/**
* Get account email from message folder (used for the authuser URL parameter)
* @param {Object} message - Message object
* @returns {Promise<string|null>} - Account email or null
*/
async function getAccountEmailFromMessage(message) {
try {
if (!message?.folder?.accountId) return null;
const account = await browser.accounts.get(message.folder.accountId);
return account?.identities?.[0]?.email || null;
} catch (error) {
console.error("[Open in Gmail] Error getting account email:", error);
return null;
}
}
/**
* Determine if a message belongs to a Gmail account (consumer or Workspace).
* Uses a two-step check: fast domain check first, then header-based fallback.
* Caches confirmed Gmail accountIds to avoid repeated getFull() calls.
* @param {Object} message - Message object
* @returns {Promise<boolean>}
*/
async function isGmailAccount(message) {
const accountId = message?.folder?.accountId;
if (!accountId) return false;
if (gmailAccountCache.has(accountId)) return true;
const accountEmail = await getAccountEmailFromMessage(message);
if (isGmailEmailDomain(accountEmail)) {
gmailAccountCache.add(accountId);
return true;
}
// Header-based fallback for Workspace accounts (custom domain + Gmail backend)
try {
const fullMessage = await browser.messages.getFull(message.id);
if (hasGmailHeaders(fullMessage)) {
gmailAccountCache.add(accountId);
return true;
}
} catch (error) {
console.error("[Open in Gmail] Error checking Gmail headers:", error);
}
return false;
}
/**
* Extract Message-ID from email headers
* @param {Object} fullMessage - Full message object with headers
* @returns {string|null} - Message-ID or null if not found
*/
function extractMessageId(fullMessage) {
if (!fullMessage || !fullMessage.headers) {
return null;
}
const messageIdHeader = fullMessage.headers['message-id'];
if (!messageIdHeader || messageIdHeader.length === 0) {
return null;
}
let messageId = Array.isArray(messageIdHeader) ? messageIdHeader[0] : messageIdHeader;
messageId = messageId.trim();
if (messageId.startsWith('<') && messageId.endsWith('>')) {
messageId = messageId.slice(1, -1);
}
return messageId;
}
/**
* Build a Gmail search URL
* @param {string} encodedQuery - Pre-encoded search query
* @param {string|null} accountEmail - The account email for authuser param
* @returns {string} - Gmail URL
*/
function buildGmailUrl(encodedQuery, accountEmail) {
const authParam = accountEmail ? `?authuser=${encodeURIComponent(accountEmail)}` : '';
return `https://mail.google.com/mail/${authParam}#search/${encodedQuery}`;
}
/**
* Construct Gmail URL using Message-ID (RFC822 message ID search)
* @param {string} messageId - The Message-ID
* @param {string|null} accountEmail - The account email for authuser param
* @returns {string} - Gmail search URL
*/
function constructGmailUrl(messageId, accountEmail) {
return buildGmailUrl(`rfc822msgid:${encodeURIComponent(messageId)}`, accountEmail);
}
/**
* Construct a fallback Gmail search URL using subject and sender
* @param {Object} message - Message object with subject and author fields
* @param {string|null} accountEmail - The account email for authuser param
* @returns {string|null} - Gmail search URL, or null if query cannot be built
*/
function constructGmailFallbackUrl(message, accountEmail) {
const subject = message.subject || "";
const author = message.author || "";
let searchQuery = "";
if (subject) {
searchQuery += `subject:"${subject}"`;
}
if (author) {
const emailMatch = author.match(/<([^>]+)>/);
const email = emailMatch ? emailMatch[1] : author;
if (searchQuery) searchQuery += " ";
searchQuery += `from:${email}`;
}
if (!searchQuery) {
return null;
}
return buildGmailUrl(encodeURIComponent(searchQuery), accountEmail);
}
/**
* Fallback: Open Gmail by searching for subject and sender
* @param {Object} message - Message object
* @param {string|null} accountEmail - The account email for authuser param
*/
async function openGmailBySubject(message, accountEmail) {
const gmailUrl = constructGmailFallbackUrl(message, accountEmail);
if (!gmailUrl) {
console.error("Cannot construct Gmail search query without subject or author");
return;
}
console.log(`[Open in Gmail] Fallback search for: ${message.subject}`);
console.log(`[Open in Gmail] Opening URL: ${gmailUrl}`);
await browser.windows.openDefaultBrowser(gmailUrl);
}
// Only set up browser listeners if we're in a WebExtension environment (not Node.js)
if (typeof browser !== 'undefined') {
async function updateButtonState(tab, message) {
if (!message) {
browser.messageDisplayAction.disable(tab.id);
return;
}
if (await isGmailAccount(message)) {
browser.messageDisplayAction.enable(tab.id);
} else {
browser.messageDisplayAction.disable(tab.id);
}
}
browser.messageDisplay.onMessageDisplayed.addListener(async (tab, message) => {
try {
await updateButtonState(tab, message);
} catch (error) {
console.error("Error checking account for message:", error);
browser.messageDisplayAction.disable(tab.id);
}
});
// onMessageDisplayed doesn't re-fire when switching back to a folder where a message
// was already selected, so we also check on folder changes.
browser.mailTabs.onDisplayedFolderChanged.addListener(async (tab, _displayedFolder) => {
try {
const message = await browser.messageDisplay.getDisplayedMessage(tab.id);
await updateButtonState(tab, message);
} catch (error) {
console.error("Error checking account on folder change:", error);
browser.messageDisplayAction.disable(tab.id);
}
});
browser.messageDisplayAction.onClicked.addListener(async (tab) => {
try {
const message = await browser.messageDisplay.getDisplayedMessage(tab.id);
if (!message) {
console.error("No message is currently displayed");
return;
}
const fullMessage = await browser.messages.getFull(message.id);
const accountEmail = await getAccountEmailFromMessage(message);
const messageId = extractMessageId(fullMessage);
if (!messageId) {
console.error("Could not extract Message-ID from email");
await openGmailBySubject(message, accountEmail);
return;
}
const gmailUrl = constructGmailUrl(messageId, accountEmail);
console.log(`[Open in Gmail] Account email: ${accountEmail}`);
console.log(`[Open in Gmail] Opening URL: ${gmailUrl}`);
await browser.windows.openDefaultBrowser(gmailUrl);
} catch (error) {
console.error("Error opening email in Gmail:", error);
}
});
}
// Export functions for testing (when running in Node.js test environment)
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
extractMessageId,
constructGmailUrl,
constructGmailFallbackUrl,
};
}