694 lines
No EOL
23 KiB
JavaScript
694 lines
No EOL
23 KiB
JavaScript
// Enhanced Thread ID Extractor for Gmail
|
|
// This provides bulletproof thread ID extraction using multiple fallback methods
|
|
|
|
class GmailThreadExtractor {
|
|
constructor() {
|
|
this.cache = new Map(); // Cache thread IDs to avoid repeated extraction
|
|
this.observers = new Set(); // Track active observers
|
|
this.lastKnownThreadId = null;
|
|
this.extractionMethods = [
|
|
this.extractFromURL,
|
|
this.extractFromDOM,
|
|
this.extractFromGmailAPI,
|
|
this.extractFromMessageHeaders,
|
|
this.extractFromBrowserHistory,
|
|
this.extractFromGmailInternals,
|
|
this.extractFromFallbackMethods
|
|
];
|
|
// Add batch-specific selectors
|
|
this.batchSelectors = {
|
|
selectedCheckboxes: 'div[role="checkbox"][aria-checked="true"]',
|
|
threadRow: 'tr[data-legacy-thread-id], tr[data-thread-id]',
|
|
subjectElement: 'span.bog',
|
|
senderElement: 'span.yX.xY span.zF, span.yX.xY span.yP'
|
|
};
|
|
}
|
|
|
|
// Main extraction method with comprehensive fallbacks
|
|
async extractThreadId(context = 'current', options = {}) {
|
|
const cacheKey = `${context}_${window.location.href}`;
|
|
|
|
// Check cache first
|
|
if (this.cache.has(cacheKey) && !options.forceRefresh) {
|
|
console.log('Thread ID found in cache:', this.cache.get(cacheKey));
|
|
return this.cache.get(cacheKey);
|
|
}
|
|
|
|
console.log(`=== EXTRACTING THREAD ID (${context}) ===`);
|
|
|
|
// Handle batch selection context differently
|
|
if (context === 'selected') {
|
|
return this.extractBatchThreadIds(options);
|
|
}
|
|
|
|
// Try each extraction method in order
|
|
for (const method of this.extractionMethods) {
|
|
try {
|
|
const threadId = await method.call(this, context, options);
|
|
if (threadId && this.validateThreadId(threadId)) {
|
|
console.log(`Thread ID found via ${method.name}:`, threadId);
|
|
this.cache.set(cacheKey, threadId);
|
|
this.lastKnownThreadId = threadId;
|
|
return threadId;
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Method ${method.name} failed:`, error);
|
|
}
|
|
}
|
|
|
|
// If all methods fail, try emergency fallbacks
|
|
const emergencyId = await this.emergencyExtraction(context);
|
|
if (emergencyId) {
|
|
console.log('Thread ID found via emergency method:', emergencyId);
|
|
return emergencyId;
|
|
}
|
|
|
|
console.error('All thread ID extraction methods failed');
|
|
return null;
|
|
}
|
|
|
|
// Specialized method for batch thread extraction
|
|
extractBatchThreadIds(options = {}) {
|
|
console.log('=== EXTRACTING BATCH THREAD IDS ===');
|
|
const selectedCheckboxes = document.querySelectorAll(this.batchSelectors.selectedCheckboxes);
|
|
const threadIds = [];
|
|
const threadInfo = [];
|
|
|
|
selectedCheckboxes.forEach((checkbox, index) => {
|
|
const row = checkbox.closest('tr');
|
|
if (!row) return;
|
|
|
|
// Try multiple extraction methods for each row
|
|
let threadId = null;
|
|
let extractionMethod = 'unknown';
|
|
|
|
// Method 1: Direct attributes
|
|
threadId = row.getAttribute('data-legacy-thread-id') || row.getAttribute('data-thread-id');
|
|
if (threadId) {
|
|
extractionMethod = 'row-attribute';
|
|
}
|
|
|
|
// Method 2: Child elements
|
|
if (!threadId) {
|
|
const threadElement = row.querySelector('[data-legacy-thread-id], [data-thread-id]');
|
|
if (threadElement) {
|
|
threadId = threadElement.getAttribute('data-legacy-thread-id') ||
|
|
threadElement.getAttribute('data-thread-id');
|
|
extractionMethod = 'child-element';
|
|
}
|
|
}
|
|
|
|
// Method 3: Row ID parsing
|
|
if (!threadId && row.id) {
|
|
const match = row.id.match(/([a-f0-9]{16})/i);
|
|
if (match && this.validateThreadId(match[1])) {
|
|
threadId = match[1];
|
|
extractionMethod = 'row-id';
|
|
}
|
|
}
|
|
|
|
// Method 4: Click and extract (if enabled)
|
|
if (!threadId && options.allowClickExtraction) {
|
|
threadId = this.extractByClicking(row);
|
|
extractionMethod = 'click-extraction';
|
|
}
|
|
|
|
if (threadId && this.validateThreadId(threadId)) {
|
|
threadIds.push(threadId);
|
|
|
|
// Collect additional info for better logging
|
|
const subject = row.querySelector(this.batchSelectors.subjectElement)?.textContent?.trim();
|
|
const sender = row.querySelector(this.batchSelectors.senderElement)?.textContent?.trim();
|
|
|
|
threadInfo.push({
|
|
threadId,
|
|
subject,
|
|
sender,
|
|
extractionMethod,
|
|
index
|
|
});
|
|
}
|
|
});
|
|
|
|
// Log detailed extraction results
|
|
console.log(`Found ${threadIds.length} thread IDs from ${selectedCheckboxes.length} selected items`);
|
|
threadInfo.forEach(info => {
|
|
console.log(`Thread ${info.index + 1}: ${info.threadId} (${info.extractionMethod}) - "${info.subject}" from ${info.sender}`);
|
|
});
|
|
|
|
// Store thread info for potential fallback matching
|
|
if (options.returnFullInfo) {
|
|
return threadInfo;
|
|
}
|
|
|
|
return [...new Set(threadIds)]; // Remove duplicates
|
|
}
|
|
|
|
// Method 1: Extract from URL (most reliable)
|
|
extractFromURL(context) {
|
|
const url = window.location.href;
|
|
const hash = window.location.hash;
|
|
|
|
// Pattern 1: #inbox/thread_id
|
|
let match = hash.match(/[#/]([a-f0-9]{16})$/i);
|
|
if (match) return match[1];
|
|
|
|
// Pattern 2: #search/query/thread_id
|
|
match = hash.match(/[#/]([a-f0-9]{16})(?:[/?]|$)/i);
|
|
if (match) return match[1];
|
|
|
|
// Pattern 3: URL parameters
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const threadParam = urlParams.get('th') || urlParams.get('thread');
|
|
if (threadParam && this.validateThreadId(threadParam)) return threadParam;
|
|
|
|
// Pattern 4: Path-based thread ID
|
|
match = url.match(/\/mail\/u\/\d+\/#[^/]*\/([a-f0-9]{16})/i);
|
|
if (match) return match[1];
|
|
|
|
// Pattern 5: Label or search views with thread ID
|
|
match = hash.match(/label\/[^/]+\/([a-f0-9]{16})/i);
|
|
if (match) return match[1];
|
|
|
|
return null;
|
|
}
|
|
|
|
// Method 2: Extract from DOM elements
|
|
extractFromDOM(context) {
|
|
const selectors = [
|
|
// Primary selectors
|
|
'[data-legacy-thread-id]',
|
|
'[data-thread-perm-id]',
|
|
'[data-thread-id]',
|
|
|
|
// Conversation view selectors
|
|
'h2[data-legacy-thread-id]',
|
|
'h2[data-thread-perm-id]',
|
|
'.nH.if [data-legacy-thread-id]',
|
|
|
|
// Message container selectors
|
|
'div[role="listitem"][data-legacy-thread-id]',
|
|
'.h7[data-legacy-thread-id]',
|
|
|
|
// Gmail-specific selectors
|
|
'.adn[data-legacy-thread-id]',
|
|
'.zA[data-legacy-thread-id]',
|
|
|
|
// Additional Gmail UI selectors
|
|
'.ae4[data-legacy-thread-id]', // Thread container
|
|
'.Cp[data-legacy-thread-id]', // Message list
|
|
'.gs[data-legacy-thread-id]', // Conversation wrapper
|
|
|
|
// Fallback selectors
|
|
'[jsaction*="thread"]',
|
|
'[data-tooltip*="thread"]'
|
|
];
|
|
|
|
// Prioritize visible elements
|
|
for (const selector of selectors) {
|
|
const elements = document.querySelectorAll(selector);
|
|
for (const element of elements) {
|
|
// Check if element is visible
|
|
if (element.offsetParent === null) continue;
|
|
|
|
const threadId = element.getAttribute('data-legacy-thread-id') ||
|
|
element.getAttribute('data-thread-perm-id') ||
|
|
element.getAttribute('data-thread-id');
|
|
|
|
if (threadId && this.validateThreadId(threadId)) {
|
|
return threadId;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for thread ID in element IDs or classes
|
|
const allElements = document.querySelectorAll('*[id*="thread"], *[class*="thread"]');
|
|
for (const element of allElements) {
|
|
const id = element.id || element.className;
|
|
const match = id.match(/([a-f0-9]{16})/i);
|
|
if (match && this.validateThreadId(match[1])) {
|
|
return match[1];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Method 3: Extract using Gmail API
|
|
async extractFromGmailAPI(context, options) {
|
|
if (!options.token) {
|
|
// Try to get token from background script
|
|
try {
|
|
const response = await new Promise((resolve) => {
|
|
chrome.runtime.sendMessage({action: 'getOAuthToken'}, resolve);
|
|
});
|
|
if (response && response.token) {
|
|
options.token = response.token;
|
|
} else {
|
|
return null;
|
|
}
|
|
} catch (error) {
|
|
console.warn('Could not get OAuth token for API extraction');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Try multiple methods to get a message ID
|
|
const messageId = this.extractMessageId() || await this.extractMessageIdFromAPI(options.token);
|
|
if (!messageId) return null;
|
|
|
|
try {
|
|
// Get message details from Gmail API
|
|
const response = await fetch(
|
|
`https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}`,
|
|
{
|
|
headers: { 'Authorization': `Bearer ${options.token}` }
|
|
}
|
|
);
|
|
|
|
if (response.ok) {
|
|
const messageData = await response.json();
|
|
return messageData.threadId;
|
|
}
|
|
} catch (error) {
|
|
console.warn('Gmail API extraction failed:', error);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Extract message ID from Gmail API (when DOM fails)
|
|
async extractMessageIdFromAPI(token) {
|
|
try {
|
|
// Get the most recent message
|
|
const response = await fetch(
|
|
'https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=1',
|
|
{
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
}
|
|
);
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.messages && data.messages.length > 0) {
|
|
return data.messages[0].id;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to get message ID from API:', error);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Method 4: Extract from message headers
|
|
extractFromMessageHeaders() {
|
|
// Look for message elements with headers
|
|
const messageElements = document.querySelectorAll('div[role="listitem"]');
|
|
|
|
for (const element of messageElements) {
|
|
// Check for thread ID in data attributes
|
|
const threadId = element.getAttribute('data-legacy-thread-id') ||
|
|
element.getAttribute('data-thread-id');
|
|
|
|
if (threadId && this.validateThreadId(threadId)) {
|
|
return threadId;
|
|
}
|
|
|
|
// Check parent containers
|
|
const parent = element.closest('[data-legacy-thread-id], [data-thread-id]');
|
|
if (parent) {
|
|
const parentThreadId = parent.getAttribute('data-legacy-thread-id') ||
|
|
parent.getAttribute('data-thread-id');
|
|
if (parentThreadId && this.validateThreadId(parentThreadId)) {
|
|
return parentThreadId;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Method 5: Extract from browser history
|
|
extractFromBrowserHistory() {
|
|
// Check if we can extract from the current history state
|
|
if (history.state && history.state.threadId) {
|
|
return history.state.threadId;
|
|
}
|
|
|
|
// Parse previous URLs in session storage
|
|
try {
|
|
const historyKey = 'gmail_thread_history';
|
|
let previousUrls = JSON.parse(sessionStorage.getItem(historyKey) || '[]');
|
|
|
|
// Add current URL to history
|
|
const currentUrl = window.location.href;
|
|
if (!previousUrls.includes(currentUrl)) {
|
|
previousUrls.push(currentUrl);
|
|
// Keep only last 50 URLs
|
|
if (previousUrls.length > 50) {
|
|
previousUrls = previousUrls.slice(-50);
|
|
}
|
|
sessionStorage.setItem(historyKey, JSON.stringify(previousUrls));
|
|
}
|
|
|
|
// Search through history
|
|
for (const url of previousUrls.reverse()) {
|
|
const match = url.match(/[#/]([a-f0-9]{16})/i);
|
|
if (match && this.validateThreadId(match[1])) {
|
|
return match[1];
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn('Could not parse thread history');
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Method 6: Extract from Gmail internals
|
|
extractFromGmailInternals() {
|
|
// Try to access Gmail's internal JavaScript objects
|
|
try {
|
|
// Gmail sometimes exposes thread data in global variables
|
|
if (window.GM_SPT_ENABLED && window.GM_ACTION_TOKEN) {
|
|
// Look for Gmail's internal state
|
|
const scripts = document.querySelectorAll('script');
|
|
for (const script of scripts) {
|
|
if (script.textContent.includes('thread_id')) {
|
|
const match = script.textContent.match(/"thread_id":"([a-f0-9]{16})"/i);
|
|
if (match && this.validateThreadId(match[1])) {
|
|
return match[1];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for thread ID in Gmail's data layer
|
|
if (window.dataLayer) {
|
|
for (const item of window.dataLayer) {
|
|
if (item.thread_id && this.validateThreadId(item.thread_id)) {
|
|
return item.thread_id;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for Gmail's GLOBALS object
|
|
if (window.GLOBALS) {
|
|
// Gmail sometimes stores thread info in GLOBALS
|
|
const threadIdPatterns = [
|
|
window.GLOBALS[17], // Thread ID location in some versions
|
|
window.GLOBALS[40], // Alternative location
|
|
];
|
|
|
|
for (const value of threadIdPatterns) {
|
|
if (value && typeof value === 'string' && this.validateThreadId(value)) {
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn('Gmail internals extraction failed:', error);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Method 7: Fallback methods
|
|
async extractFromFallbackMethods(context) {
|
|
// Try to trigger Gmail to reveal thread ID
|
|
await this.triggerGmailStateUpdate();
|
|
|
|
// Wait a bit and try DOM extraction again
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
const domResult = this.extractFromDOM(context);
|
|
if (domResult) return domResult;
|
|
|
|
// Try to extract from any visible thread indicators
|
|
const threadIndicators = document.querySelectorAll('[title*="thread"], [aria-label*="thread"]');
|
|
for (const indicator of threadIndicators) {
|
|
const text = indicator.title || indicator.getAttribute('aria-label') || '';
|
|
const match = text.match(/([a-f0-9]{16})/i);
|
|
if (match && this.validateThreadId(match[1])) {
|
|
return match[1];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Emergency extraction when all else fails
|
|
async emergencyExtraction(context) {
|
|
console.log('Attempting emergency thread ID extraction...');
|
|
|
|
// Method 1: Force Gmail to navigate and extract from URL change
|
|
if (context === 'current') {
|
|
const currentUrl = window.location.href;
|
|
|
|
// Try clicking on the current conversation to force URL update
|
|
const conversationElement = document.querySelector('.zA.zE, .zA.yW, div[role="listitem"]');
|
|
if (conversationElement) {
|
|
conversationElement.click();
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
const newThreadId = this.extractFromURL();
|
|
if (newThreadId) return newThreadId;
|
|
|
|
// Restore original state if possible
|
|
if (window.location.href !== currentUrl) {
|
|
history.back();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 2: Use MutationObserver to catch thread ID when it appears
|
|
return new Promise((resolve) => {
|
|
const observer = new MutationObserver((mutations) => {
|
|
for (const mutation of mutations) {
|
|
for (const node of mutation.addedNodes) {
|
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
const threadId = this.extractThreadIdFromElement(node);
|
|
if (threadId) {
|
|
observer.disconnect();
|
|
resolve(threadId);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
subtree: true,
|
|
attributes: true,
|
|
attributeFilter: ['data-legacy-thread-id', 'data-thread-id', 'data-thread-perm-id']
|
|
});
|
|
|
|
// Timeout after 5 seconds
|
|
setTimeout(() => {
|
|
observer.disconnect();
|
|
resolve(null);
|
|
}, 5000);
|
|
});
|
|
}
|
|
|
|
// Helper: Extract thread ID by clicking (for batch operations)
|
|
extractByClicking(row) {
|
|
try {
|
|
// Store current URL
|
|
const originalUrl = window.location.href;
|
|
|
|
// Click the row
|
|
row.click();
|
|
|
|
// Wait for navigation
|
|
const startTime = Date.now();
|
|
while (Date.now() - startTime < 1000) {
|
|
if (window.location.href !== originalUrl) {
|
|
const threadId = this.extractFromURL();
|
|
if (threadId) {
|
|
// Go back to inbox
|
|
history.back();
|
|
return threadId;
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn('Click extraction failed:', error);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Helper: Extract thread ID from any element
|
|
extractThreadIdFromElement(element) {
|
|
if (!element.querySelector) return null;
|
|
|
|
const threadElement = element.querySelector('[data-legacy-thread-id], [data-thread-id], [data-thread-perm-id]');
|
|
if (threadElement) {
|
|
const threadId = threadElement.getAttribute('data-legacy-thread-id') ||
|
|
threadElement.getAttribute('data-thread-id') ||
|
|
threadElement.getAttribute('data-thread-perm-id');
|
|
|
|
if (threadId && this.validateThreadId(threadId)) {
|
|
return threadId;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Helper: Extract message ID from current view
|
|
extractMessageId() {
|
|
const selectors = [
|
|
'[data-legacy-message-id]',
|
|
'[data-message-id]',
|
|
'[data-internaldate]', // Sometimes message ID is in internal date element
|
|
'div[role="listitem"][id]' // Message container with ID
|
|
];
|
|
|
|
for (const selector of selectors) {
|
|
const elements = document.querySelectorAll(selector);
|
|
for (const element of elements) {
|
|
const messageId = element.getAttribute('data-legacy-message-id') ||
|
|
element.getAttribute('data-message-id') ||
|
|
element.id;
|
|
|
|
if (messageId && messageId.length > 10) { // Basic validation
|
|
return messageId;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Helper: Trigger Gmail to update its state
|
|
async triggerGmailStateUpdate() {
|
|
// Trigger events that might cause Gmail to update its DOM
|
|
const events = ['focus', 'click', 'keydown'];
|
|
for (const eventType of events) {
|
|
document.dispatchEvent(new Event(eventType, { bubbles: true }));
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
}
|
|
|
|
// Try pressing 'j' then 'k' to navigate messages
|
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'j' }));
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k' }));
|
|
}
|
|
|
|
// Validate thread ID format
|
|
validateThreadId(threadId) {
|
|
if (!threadId || typeof threadId !== 'string') return false;
|
|
|
|
// Gmail thread IDs are typically 16 character hexadecimal strings
|
|
return /^[a-f0-9]{16}$/i.test(threadId);
|
|
}
|
|
|
|
// Clear cache
|
|
clearCache() {
|
|
this.cache.clear();
|
|
}
|
|
|
|
// Set up continuous monitoring
|
|
startMonitoring() {
|
|
// Monitor URL changes
|
|
let lastUrl = window.location.href;
|
|
const urlObserver = new MutationObserver(() => {
|
|
if (window.location.href !== lastUrl) {
|
|
lastUrl = window.location.href;
|
|
this.clearCache(); // Clear cache on navigation
|
|
|
|
// Store URL in history
|
|
try {
|
|
const historyKey = 'gmail_thread_history';
|
|
let urls = JSON.parse(sessionStorage.getItem(historyKey) || '[]');
|
|
if (!urls.includes(window.location.href)) {
|
|
urls.push(window.location.href);
|
|
if (urls.length > 50) urls = urls.slice(-50);
|
|
sessionStorage.setItem(historyKey, JSON.stringify(urls));
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to update thread history:', e);
|
|
}
|
|
}
|
|
});
|
|
|
|
urlObserver.observe(document.body, { childList: true, subtree: true });
|
|
this.observers.add(urlObserver);
|
|
|
|
// Monitor for new thread elements
|
|
const threadObserver = new MutationObserver((mutations) => {
|
|
for (const mutation of mutations) {
|
|
for (const node of mutation.addedNodes) {
|
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
const threadId = this.extractThreadIdFromElement(node);
|
|
if (threadId && threadId !== this.lastKnownThreadId) {
|
|
this.lastKnownThreadId = threadId;
|
|
// Dispatch custom event for other parts of the extension
|
|
document.dispatchEvent(new CustomEvent('threadIdDetected', {
|
|
detail: { threadId }
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
threadObserver.observe(document.body, {
|
|
childList: true,
|
|
subtree: true,
|
|
attributes: true,
|
|
attributeFilter: ['data-legacy-thread-id', 'data-thread-id', 'data-thread-perm-id']
|
|
});
|
|
|
|
this.observers.add(threadObserver);
|
|
|
|
console.log('Thread monitoring started');
|
|
}
|
|
|
|
// Clean up observers
|
|
stopMonitoring() {
|
|
for (const observer of this.observers) {
|
|
observer.disconnect();
|
|
}
|
|
this.observers.clear();
|
|
console.log('Thread monitoring stopped');
|
|
}
|
|
}
|
|
|
|
// Create global instance
|
|
const threadExtractor = new GmailThreadExtractor();
|
|
|
|
// Start monitoring for thread changes
|
|
threadExtractor.startMonitoring();
|
|
|
|
// Extract thread ID with all fallbacks
|
|
async function getThreadIdReliably(options = {}) {
|
|
const threadId = await threadExtractor.extractThreadId('current', options);
|
|
|
|
if (!threadId) {
|
|
throw new Error('Could not extract thread ID after trying all methods');
|
|
}
|
|
|
|
return threadId;
|
|
}
|
|
|
|
// Backward compatibility functions
|
|
function extractThreadId(context = 'current') {
|
|
return threadExtractor.extractThreadId(context);
|
|
}
|
|
|
|
function getSelectedThreadIds() {
|
|
return threadExtractor.extractThreadId('selected');
|
|
}
|
|
|
|
// Export for use in other parts of the extension
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = {
|
|
GmailThreadExtractor,
|
|
getThreadIdReliably,
|
|
threadExtractor,
|
|
extractThreadId,
|
|
getSelectedThreadIds
|
|
};
|
|
}
|