289 lines
8.4 KiB
Markdown
289 lines
8.4 KiB
Markdown
|
|
# Bulletproof Gmail Thread ID Extraction Strategies
|
||
|
|
|
||
|
|
## Core Principles
|
||
|
|
|
||
|
|
### 1. **Layered Fallback System**
|
||
|
|
- Never rely on a single method
|
||
|
|
- Each method should be independent and handle different failure scenarios
|
||
|
|
- Graceful degradation when methods fail
|
||
|
|
|
||
|
|
### 2. **Caching & Performance**
|
||
|
|
- Cache successful extractions to avoid repeated work
|
||
|
|
- Invalidate cache on navigation or DOM changes
|
||
|
|
- Use session storage for persistence across page reloads
|
||
|
|
|
||
|
|
### 3. **Validation & Verification**
|
||
|
|
- Always validate thread ID format (16-char hex for Gmail)
|
||
|
|
- Cross-verify using multiple methods when possible
|
||
|
|
- Implement confidence scoring for extraction methods
|
||
|
|
|
||
|
|
## Advanced Techniques
|
||
|
|
|
||
|
|
### 4. **Gmail API Integration**
|
||
|
|
```javascript
|
||
|
|
// Use Gmail API as authoritative source
|
||
|
|
async function verifyThreadIdWithAPI(threadId, token) {
|
||
|
|
try {
|
||
|
|
const response = await fetch(
|
||
|
|
`https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}`,
|
||
|
|
{ headers: { 'Authorization': `Bearer ${token}` } }
|
||
|
|
);
|
||
|
|
return response.ok;
|
||
|
|
} catch {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5. **Proactive Thread Tracking**
|
||
|
|
```javascript
|
||
|
|
// Track thread IDs as user navigates
|
||
|
|
class ThreadTracker {
|
||
|
|
constructor() {
|
||
|
|
this.threadHistory = new Map();
|
||
|
|
this.setupNavigationTracking();
|
||
|
|
}
|
||
|
|
|
||
|
|
setupNavigationTracking() {
|
||
|
|
// Track URL changes
|
||
|
|
let lastUrl = location.href;
|
||
|
|
new MutationObserver(() => {
|
||
|
|
if (location.href !== lastUrl) {
|
||
|
|
this.onNavigationChange(lastUrl, location.href);
|
||
|
|
lastUrl = location.href;
|
||
|
|
}
|
||
|
|
}).observe(document, { subtree: true, childList: true });
|
||
|
|
}
|
||
|
|
|
||
|
|
onNavigationChange(oldUrl, newUrl) {
|
||
|
|
const threadId = this.extractFromUrl(newUrl);
|
||
|
|
if (threadId) {
|
||
|
|
this.threadHistory.set(newUrl, {
|
||
|
|
threadId,
|
||
|
|
timestamp: Date.now(),
|
||
|
|
confidence: 'high'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 6. **DOM Mutation Monitoring**
|
||
|
|
```javascript
|
||
|
|
// Watch for thread ID attributes being added
|
||
|
|
function setupThreadIdWatcher() {
|
||
|
|
const observer = new MutationObserver((mutations) => {
|
||
|
|
mutations.forEach((mutation) => {
|
||
|
|
if (mutation.type === 'attributes') {
|
||
|
|
const threadId = mutation.target.getAttribute('data-legacy-thread-id');
|
||
|
|
if (threadId && validateThreadId(threadId)) {
|
||
|
|
// Store for immediate use
|
||
|
|
window.currentThreadId = threadId;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
observer.observe(document.body, {
|
||
|
|
attributes: true,
|
||
|
|
attributeFilter: ['data-legacy-thread-id', 'data-thread-id'],
|
||
|
|
subtree: true
|
||
|
|
});
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 7. **Cross-Frame Communication**
|
||
|
|
```javascript
|
||
|
|
// Handle Gmail's iframe structure
|
||
|
|
function extractFromAllFrames() {
|
||
|
|
const frames = [window, ...Array.from(document.querySelectorAll('iframe'))
|
||
|
|
.map(f => f.contentWindow)
|
||
|
|
.filter(w => w && w.location.hostname.includes('google'))];
|
||
|
|
|
||
|
|
for (const frame of frames) {
|
||
|
|
try {
|
||
|
|
const threadId = extractThreadIdFromFrame(frame);
|
||
|
|
if (threadId) return threadId;
|
||
|
|
} catch (e) {
|
||
|
|
// Cross-origin restrictions
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 8. **Intelligent Retry Logic**
|
||
|
|
```javascript
|
||
|
|
async function extractWithRetry(maxAttempts = 5, delay = 1000) {
|
||
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||
|
|
const threadId = await threadExtractor.extractThreadId();
|
||
|
|
|
||
|
|
if (threadId) {
|
||
|
|
return threadId;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (attempt < maxAttempts) {
|
||
|
|
// Exponential backoff
|
||
|
|
await new Promise(resolve => setTimeout(resolve, delay * attempt));
|
||
|
|
|
||
|
|
// Try to trigger Gmail state updates
|
||
|
|
await triggerGmailRefresh();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new Error('Failed to extract thread ID after all attempts');
|
||
|
|
}
|
||
|
|
|
||
|
|
async function triggerGmailRefresh() {
|
||
|
|
// Simulate user interactions that might refresh Gmail's state
|
||
|
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'r' }));
|
||
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 9. **Context-Aware Extraction**
|
||
|
|
```javascript
|
||
|
|
function getExtractionContext() {
|
||
|
|
return {
|
||
|
|
isInboxView: location.hash.includes('#inbox'),
|
||
|
|
isConversationView: !!document.querySelector('div[role="listitem"]'),
|
||
|
|
isComposeView: !!document.querySelector('div[aria-label="Message Body"]'),
|
||
|
|
gmailVersion: detectGmailVersion(),
|
||
|
|
userAgent: navigator.userAgent
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function extractBasedOnContext(context) {
|
||
|
|
if (context.isConversationView) {
|
||
|
|
return extractFromConversationView();
|
||
|
|
} else if (context.isInboxView) {
|
||
|
|
return extractFromInboxView();
|
||
|
|
} else {
|
||
|
|
return extractGeneric();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 10. **Error Recovery & Logging**
|
||
|
|
```javascript
|
||
|
|
class ThreadExtractionLogger {
|
||
|
|
constructor() {
|
||
|
|
this.attempts = [];
|
||
|
|
this.successes = new Map();
|
||
|
|
this.failures = new Map();
|
||
|
|
}
|
||
|
|
|
||
|
|
logAttempt(method, success, threadId, error) {
|
||
|
|
const attempt = {
|
||
|
|
method,
|
||
|
|
success,
|
||
|
|
threadId,
|
||
|
|
error: error?.message,
|
||
|
|
timestamp: Date.now(),
|
||
|
|
url: location.href,
|
||
|
|
context: getExtractionContext()
|
||
|
|
};
|
||
|
|
|
||
|
|
this.attempts.push(attempt);
|
||
|
|
|
||
|
|
if (success) {
|
||
|
|
this.successes.set(method, (this.successes.get(method) || 0) + 1);
|
||
|
|
} else {
|
||
|
|
this.failures.set(method, (this.failures.get(method) || 0) + 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
getReliabilityStats() {
|
||
|
|
const stats = {};
|
||
|
|
for (const [method, successes] of this.successes) {
|
||
|
|
const failures = this.failures.get(method) || 0;
|
||
|
|
stats[method] = {
|
||
|
|
successRate: successes / (successes + failures),
|
||
|
|
totalAttempts: successes + failures
|
||
|
|
};
|
||
|
|
}
|
||
|
|
return stats;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Implementation Best Practices
|
||
|
|
|
||
|
|
### 11. **Performance Optimization**
|
||
|
|
- Use `requestIdleCallback` for non-critical extractions
|
||
|
|
- Debounce rapid extraction requests
|
||
|
|
- Implement method prioritization based on success rates
|
||
|
|
|
||
|
|
### 12. **User Experience**
|
||
|
|
- Show loading states during extraction
|
||
|
|
- Provide fallback UI when thread ID cannot be found
|
||
|
|
- Allow manual thread ID input as last resort
|
||
|
|
|
||
|
|
### 13. **Testing & Validation**
|
||
|
|
```javascript
|
||
|
|
// Comprehensive testing suite
|
||
|
|
const testCases = [
|
||
|
|
{ url: '#inbox/1234567890abcdef', expected: '1234567890abcdef' },
|
||
|
|
{ url: '#search/test/1234567890abcdef', expected: '1234567890abcdef' },
|
||
|
|
// Add more test cases for different Gmail states
|
||
|
|
];
|
||
|
|
|
||
|
|
function runExtractionTests() {
|
||
|
|
for (const testCase of testCases) {
|
||
|
|
// Simulate different Gmail states and verify extraction
|
||
|
|
history.pushState(null, '', testCase.url);
|
||
|
|
const extracted = threadExtractor.extractThreadId();
|
||
|
|
console.assert(extracted === testCase.expected,
|
||
|
|
`Failed for ${testCase.url}: got ${extracted}, expected ${testCase.expected}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 14. **Graceful Degradation**
|
||
|
|
```javascript
|
||
|
|
// When all else fails, provide alternative functionality
|
||
|
|
async function handleExtractionFailure() {
|
||
|
|
// Option 1: Use Gmail API to list recent threads
|
||
|
|
const recentThreads = await fetchRecentThreads();
|
||
|
|
|
||
|
|
// Option 2: Show thread selection UI
|
||
|
|
showThreadSelectionDialog(recentThreads);
|
||
|
|
|
||
|
|
// Option 3: Generate draft without thread context
|
||
|
|
return generateStandaloneDraft();
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Monitoring & Maintenance
|
||
|
|
|
||
|
|
### 15. **Success Rate Tracking**
|
||
|
|
- Monitor extraction success rates across different Gmail versions
|
||
|
|
- Track which methods work best in different contexts
|
||
|
|
- Automatically adjust method priority based on performance
|
||
|
|
|
||
|
|
### 16. **Gmail Update Detection**
|
||
|
|
```javascript
|
||
|
|
// Detect when Gmail updates might break extraction
|
||
|
|
function detectGmailChanges() {
|
||
|
|
const currentSelectors = document.querySelectorAll('[data-legacy-thread-id]').length;
|
||
|
|
const storedCount = localStorage.getItem('gmail_thread_elements');
|
||
|
|
|
||
|
|
if (storedCount && Math.abs(currentSelectors - parseInt(storedCount)) > 10) {
|
||
|
|
console.warn('Significant change in Gmail DOM structure detected');
|
||
|
|
// Trigger extraction method re-evaluation
|
||
|
|
reevaluateExtractionMethods();
|
||
|
|
}
|
||
|
|
|
||
|
|
localStorage.setItem('gmail_thread_elements', currentSelectors.toString());
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
This comprehensive approach ensures that your extension can reliably extract Gmail thread IDs regardless of:
|
||
|
|
- Gmail interface updates
|
||
|
|
- Different Gmail views (inbox, conversation, search)
|
||
|
|
- Network issues affecting API calls
|
||
|
|
- DOM structure changes
|
||
|
|
- User navigation patterns
|
||
|
|
|
||
|
|
The key is having multiple independent methods that can work together, with intelligent fallbacks and continuous monitoring to adapt to changes in Gmail's structure.
|