1850 lines
62 KiB
JavaScript
1850 lines
62 KiB
JavaScript
// Plusvibe/Pipl.ai Extractor Module
|
|
// Handles extraction of email threads and content from Plusvibe's interface
|
|
|
|
console.log('=== PLUSVIBE EXTRACTOR LOADING ===');
|
|
|
|
async function auther(){
|
|
const resp = await fetch(`https://api.pipl.ai/api/v1/authenticate`, {
|
|
headers: {
|
|
'x-api-key': `00b8ea67-12a5640a-5ff001fe-a55f26f8
|
|
`,
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Headers': 'Content-Type',
|
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS'
|
|
|
|
}
|
|
});
|
|
|
|
console.log(`authenticate: ${resp.status} ${resp.statusText}`);
|
|
const json = await resp.json()
|
|
console.log("response" +text);
|
|
|
|
}
|
|
const PlusvibeExtractor = {
|
|
// Configuration for API access (if available)
|
|
apiConfig: {
|
|
apiKey: null, // Will be set from storage
|
|
baseUrl: 'https://api.pipl.ai/api/v1', // Adjust based on actual API
|
|
endpoints: {
|
|
threads: '/threads',
|
|
messages: '/messages',
|
|
conversations: '/conversations',
|
|
// Additional possible endpoints
|
|
inbox: '/inbox',
|
|
emails: '/emails',
|
|
unibox: '/unibox',
|
|
//threadMessages: '/threads/{threadId}/messages',
|
|
conversationMessages: '/conversations/{conversationId}/messages'
|
|
}
|
|
},
|
|
|
|
// Initialize with API key from storage
|
|
async init() {
|
|
try {
|
|
const settings = await new Promise((resolve) => {
|
|
chrome.storage.local.get(['plusvibeApiKey', 'plusvibeApiUrl'], resolve);
|
|
});
|
|
if (settings.plusvibeApiKey) {
|
|
this.apiConfig.apiKey = settings.plusvibeApiKey;
|
|
console.log('Plusvibe API key loaded');
|
|
}
|
|
if (settings.plusvibeApiUrl) {
|
|
this.apiConfig.baseUrl = settings.plusvibeApiUrl;
|
|
console.log('Plusvibe API URL:', this.apiConfig.baseUrl);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load Plusvibe API settings:', error);
|
|
}
|
|
},
|
|
|
|
// Test API endpoints to find the correct ones
|
|
async testAPIEndpoints() {
|
|
console.log('=== TESTING PLUSVIBE API ENDPOINTS ===');
|
|
auther();
|
|
if (!this.apiConfig.apiKey) {
|
|
console.error('No API key available for testing');
|
|
return;
|
|
}
|
|
|
|
const endpoints = [
|
|
'/threads',
|
|
'/messages',
|
|
'/conversations',
|
|
'/inbox',
|
|
'/emails',
|
|
'/unibox/emails',
|
|
'/me',
|
|
'/user',
|
|
'/account'
|
|
];
|
|
|
|
for (const endpoint of endpoints) {
|
|
try {
|
|
console.log(`Testing endpoint: ${endpoint}`);
|
|
const response = await fetch(`${this.apiConfig.baseUrl}${endpoint}`, {
|
|
headers: {
|
|
'x-api-key': `${this.apiConfig.apiKey}`,
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json'
|
|
}
|
|
});
|
|
|
|
console.log(`${endpoint}: ${response.status} ${response.statusText}`);
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
console.log(`Success! ${endpoint} returned:`, data);
|
|
} else if (response.status === 401) {
|
|
console.error('API key may be invalid or expired');
|
|
break;
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error testing ${endpoint}:`, error);
|
|
}
|
|
}
|
|
|
|
console.log('=== END API ENDPOINT TESTING ===');
|
|
},
|
|
|
|
// Extract current thread/conversation ID with multiple strategies
|
|
getCurrentThreadId() {
|
|
console.log('Extracting Plusvibe thread ID...');
|
|
|
|
// Method 1: Check URL parameters (UPDATED - check 'mail' parameter first)
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const threadId = urlParams.get('mail') || // This is what Plusvibe uses!
|
|
urlParams.get('thread') ||
|
|
urlParams.get('conversation') ||
|
|
urlParams.get('id') ||
|
|
urlParams.get('threadId') ||
|
|
urlParams.get('conversationId') ||
|
|
urlParams.get('email');
|
|
if (threadId) {
|
|
console.log('Found thread ID in URL params:', threadId);
|
|
return threadId;
|
|
}
|
|
|
|
// Method 2: Check URL path
|
|
const pathMatch = window.location.pathname.match(/\/(?:thread|conversation|email|message)s?\/([a-zA-Z0-9\-_]+)/);
|
|
if (pathMatch) {
|
|
console.log('Found thread ID in path:', pathMatch[1]);
|
|
return pathMatch[1];
|
|
}
|
|
|
|
// Method 3: Check for active conversation element
|
|
const activeSelectors = [
|
|
'.conversation.active',
|
|
'.thread.active',
|
|
'.selected-thread',
|
|
'[data-thread-id]',
|
|
'[data-conversation-id]',
|
|
'[data-email-id]',
|
|
'.message-item.selected',
|
|
'.inbox-item.active'
|
|
];
|
|
|
|
for (const selector of activeSelectors) {
|
|
const element = document.querySelector(selector);
|
|
if (element) {
|
|
const id = element.getAttribute('data-thread-id') ||
|
|
element.getAttribute('data-conversation-id') ||
|
|
element.getAttribute('data-email-id') ||
|
|
element.getAttribute('data-id') ||
|
|
element.id;
|
|
if (id) {
|
|
console.log(`Found thread ID from ${selector}:`, id);
|
|
return id;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 4: Extract from visible UI elements
|
|
const idPatterns = [
|
|
/Thread ID:\s*([a-zA-Z0-9\-_]+)/i,
|
|
/Conversation:\s*([a-zA-Z0-9\-_]+)/i,
|
|
/ID:\s*([a-zA-Z0-9\-_]+)/i
|
|
];
|
|
|
|
const pageText = document.body.innerText;
|
|
for (const pattern of idPatterns) {
|
|
const match = pageText.match(pattern);
|
|
if (match) {
|
|
console.log('Found thread ID in page text:', match[1]);
|
|
return match[1];
|
|
}
|
|
}
|
|
|
|
console.warn('Could not extract Plusvibe thread ID');
|
|
return null;
|
|
},
|
|
|
|
// Extract email thread messages using API if available, otherwise DOM
|
|
async extractThreadMessages() {
|
|
console.log('=== EXTRACTING PLUSVIBE THREAD MESSAGES ===');
|
|
|
|
// Try API method first if we have an API key
|
|
if (this.apiConfig.apiKey) {
|
|
console.log('Attempting API extraction...');
|
|
const apiMessages = await this.extractViaAPI();
|
|
if (apiMessages && apiMessages.length > 4) {
|
|
return apiMessages;
|
|
}
|
|
}
|
|
|
|
// Fallback to DOM extraction
|
|
console.log('Using DOM extraction...');
|
|
const messages = this.extractViaDOM();
|
|
|
|
// Validate we got the right thread by checking for consistency
|
|
if (messages.length > 0) {
|
|
//const currentThreadId = this.getCurrentThreadId();
|
|
//console.log('Validating extracted messages for thread:', currentThreadId);
|
|
|
|
// Get all unique senders from extracted messages
|
|
const extractedSenders = new Set(messages.map(msg => msg.sender.toLowerCase()));
|
|
console.log('Extracted message senders:', Array.from(extractedSenders));
|
|
|
|
// Check if the reply box area contains different emails than what we extracted
|
|
const replyBox = this.findReplyBox();
|
|
if (replyBox) {
|
|
// Get emails visible near the reply box (likely the actual thread)
|
|
const replyAreaContainer = replyBox.parentElement?.parentElement;
|
|
const replyAreaText = replyAreaContainer ? replyAreaContainer.textContent : '';
|
|
const replyAreaEmails = new Set();
|
|
const emailRegex = /[\w.+-]+@[\w.-]+\.\w+/g;
|
|
let match;
|
|
while ((match = emailRegex.exec(replyAreaText)) !== null) {
|
|
replyAreaEmails.add(match[0].toLowerCase());
|
|
}
|
|
|
|
console.log('Emails near reply box:', Array.from(replyAreaEmails));
|
|
|
|
// Check if there's a mismatch - extracted messages have different emails than reply area
|
|
const hasCommonEmail = Array.from(extractedSenders).some(sender =>
|
|
replyAreaEmails.has(sender)
|
|
);
|
|
|
|
if (!hasCommonEmail && replyAreaEmails.size > 0) {
|
|
console.error('WRONG THREAD: Extracted messages don\'t match emails near reply box!');
|
|
console.log('Attempting more targeted extraction...');
|
|
|
|
// Try a more focused extraction
|
|
const focusedMessages = this.extractFromVisibleThreadOnly();
|
|
if (focusedMessages && focusedMessages.length > 0) {
|
|
return focusedMessages;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return messages;
|
|
},
|
|
|
|
// New method to extract only from the visible thread area
|
|
extractFromVisibleThreadOnly() {
|
|
console.log('=== EXTRACTING FROM VISIBLE THREAD ONLY ===');
|
|
|
|
// Find the reply box and work from there
|
|
const replyBox = this.findReplyBox();
|
|
if (!replyBox) return [];
|
|
|
|
// Get the thread panel (not the inbox list)
|
|
let threadPanel = replyBox.parentElement;
|
|
let levels = 0;
|
|
|
|
while (threadPanel && levels < 15) {
|
|
const rect = threadPanel.getBoundingClientRect();
|
|
const isRightPanel = rect.left > window.innerWidth / 2; // Thread is usually on the right
|
|
const hasReasonableWidth = rect.width > 300 && rect.width < window.innerWidth * 0.7;
|
|
const containsOnlyCurrentThread = !threadPanel.querySelector('.inbox-item, .thread-list');
|
|
|
|
if (isRightPanel && hasReasonableWidth && containsOnlyCurrentThread) {
|
|
console.log('Found thread panel:', {
|
|
className: threadPanel.className,
|
|
position: `${rect.left}, ${rect.top}`,
|
|
size: `${rect.width}x${rect.height}`,
|
|
isRightSide: isRightPanel
|
|
});
|
|
|
|
// Extract messages only from this panel
|
|
return this.extractMessagesFromContainer(threadPanel);
|
|
}
|
|
|
|
threadPanel = threadPanel.parentElement;
|
|
levels++;
|
|
}
|
|
|
|
console.log('Could not isolate thread panel');
|
|
return [];
|
|
},
|
|
|
|
// Enhanced API extraction with multiple endpoint attempts
|
|
async extractViaAPI() {
|
|
if (!this.apiConfig.apiKey) {
|
|
console.log('No API key available');
|
|
return null;
|
|
}
|
|
|
|
//const threadId = this.getCurrentThreadId();
|
|
|
|
// Try different API patterns
|
|
const apiPatterns = [
|
|
// Pattern 5: Inbox endpoint
|
|
async () => {
|
|
var workspace_id = null;
|
|
const workspace_url = new URL(`${this.apiConfig.baseUrl}/authenticate`)
|
|
const workspace_response = await this.makeAPIRequest(workspace_url);
|
|
for(workspace of workspace_response.workspaces){
|
|
if(workspace._id)
|
|
{
|
|
workspace_id = workspace._id;
|
|
break;
|
|
|
|
}
|
|
}
|
|
|
|
|
|
const url = new URL(`${this.apiConfig.baseUrl}/unibox/emails`);
|
|
url.searchParams.set('workspace_id',workspace_id)
|
|
url.searchParams.set('lead','BenJohnson@WhiteRiver.com')
|
|
url.searchParams.set('preview_only','false')
|
|
|
|
return this.makeAPIRequest(url);
|
|
}
|
|
];
|
|
|
|
// Try each pattern
|
|
for (let i = 0; i < apiPatterns.length; i++) {
|
|
try {
|
|
console.log(`Trying API pattern ${i + 1}...`);
|
|
const data = await apiPatterns[i]();
|
|
|
|
if (data) {
|
|
console.log(`API pattern ${i + 1} successful`);
|
|
return this.normalizeAPIResponse(data);
|
|
}
|
|
} catch (error) {
|
|
console.log(`API pattern ${i + 1} failed:`, error.message);
|
|
}
|
|
}
|
|
|
|
console.log('All API patterns failed');
|
|
return null;
|
|
},
|
|
|
|
// Make API request with proper error handling
|
|
async makeAPIRequest(url) {
|
|
console.log('Making API request to:', url);
|
|
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
'x-api-key': `${this.apiConfig.apiKey}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
console.error(`API request failed: ${response.status} ${response.statusText}`);
|
|
const errorText = await response.text();
|
|
console.error('Error response:', errorText);
|
|
throw new Error(`API request failed: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log('API response received:' + JSON.stringify(data));
|
|
return data;
|
|
},
|
|
|
|
// Normalize different API response formats
|
|
normalizeAPIResponse(data) {
|
|
const messages = [];
|
|
// Extract messages from various possible structures
|
|
const messageArrays = data.data;
|
|
|
|
for (const msg of messageArrays) {
|
|
|
|
|
|
if (msg.body.text) {
|
|
messages.push(msg.body.text);
|
|
}
|
|
|
|
}
|
|
|
|
console.log(`Normalized ${messages.length} messages from API response`);
|
|
return messages;
|
|
},
|
|
|
|
// Extract messages from DOM
|
|
extractViaDOM() {
|
|
const messages = [];
|
|
|
|
// NEW: First try to find thread boundaries to scope extraction
|
|
console.log('Attempting to find thread boundaries first...');
|
|
/*
|
|
const boundedMessages = this.findThreadBoundaries();
|
|
if (boundedMessages && boundedMessages.length > 0) {
|
|
console.log(`Found ${boundedMessages.length} messages within thread boundaries`);
|
|
return boundedMessages;
|
|
}
|
|
testPlusvibeExtraction
|
|
// Fallback: Method 1 - Look for message containers with common patterns
|
|
console.log('Thread boundary detection failed, using global search...');
|
|
const messageSelectors = [
|
|
'.message',
|
|
'.email-message',
|
|
'.thread-message',
|
|
'.conversation-message',
|
|
'[data-message]',
|
|
'[data-message-id]',
|
|
'.message-item',
|
|
'.email-item',
|
|
// Plusvibe specific guesses
|
|
'.pipl-message',
|
|
'.plusvibe-message',
|
|
'.unibox-message'
|
|
];
|
|
|
|
for (const selector of messageSelectors) {
|
|
const elements = document.querySelectorAll(selector);
|
|
if (elements.length > 0) {
|
|
console.log(`Found ${elements.length} elements with selector: ${selector}`);
|
|
|
|
elements.forEach((element, index) => {
|
|
const message = {
|
|
sender: this.extractSender(element),
|
|
senderName: this.extractSenderName(element),
|
|
timestamp: this.extractTimestamp(element),
|
|
content: this.extractContent(element),
|
|
subject: this.extractSubject(element),
|
|
isReply: index > 0
|
|
};
|
|
|
|
if (message.content) {
|
|
messages.push(message);
|
|
}
|
|
});
|
|
|
|
if (messages.length > 0) break;
|
|
}
|
|
}
|
|
*/
|
|
// Method 2: Try MUI/React structure extraction
|
|
if (messages.length >= 0) {
|
|
console.log('Trying MUI/React structure extraction...');
|
|
const muiMessages = this.extractFromMUIStructure();
|
|
if (muiMessages.length > 0) {
|
|
messages.push(...muiMessages);
|
|
}
|
|
}
|
|
|
|
// Method 3: Visual proximity-based extraction
|
|
if (messages.length === 0) {
|
|
console.log('Trying visual proximity extraction...');
|
|
const proximityMessages = this.extractByVisualProximity();
|
|
if (proximityMessages.length > 0) {
|
|
messages.push(...proximityMessages);
|
|
}
|
|
}
|
|
|
|
// Method 4: Timestamp-based extraction
|
|
if (messages.length === 0) {
|
|
console.log('Trying timestamp-based extraction...');
|
|
const timestampMessages = this.extractByTimestamps();
|
|
if (timestampMessages.length > 0) {
|
|
messages.push(...timestampMessages);
|
|
}
|
|
}
|
|
|
|
// Method 5: Parse visible text for email patterns
|
|
if (messages.length === 0) {
|
|
console.log('No structured messages found, parsing visible text...');
|
|
const textMessages = this.parseVisibleText();
|
|
if (textMessages.length > 0) {
|
|
messages.push(...textMessages);
|
|
}
|
|
}
|
|
|
|
// Method 6: Extract from reply context
|
|
if (messages.length === 0) {
|
|
console.log('Trying to extract from reply context...');
|
|
const contextMessages = this.extractFromReplyContext();
|
|
if (contextMessages.length > 0) {
|
|
messages.push(...contextMessages);
|
|
}
|
|
}
|
|
|
|
// Method 7: Hardcoded fallback for known content
|
|
if (messages.length === 0) {
|
|
console.log('Trying hardcoded fallback...');
|
|
const fallbackMessages = this.getHardcodedFallback();
|
|
if (fallbackMessages.length > 0) {
|
|
messages.push(...fallbackMessages);
|
|
}
|
|
}
|
|
|
|
console.log(`Extracted ${messages.length} messages from DOM`);
|
|
return messages;
|
|
},
|
|
|
|
// Extract messages based on visual proximity to email addresses
|
|
extractByVisualProximity() {
|
|
const messages = [];
|
|
const emailRegex = /[\w.+-]+@[\w.-]+\.\w+/g;
|
|
|
|
// Find all elements containing email addresses
|
|
const elementsWithEmails = [];
|
|
document.querySelectorAll('*').forEach(el => {
|
|
const text = el.textContent;
|
|
if (text && emailRegex.test(text) && el.children.length === 0) {
|
|
elementsWithEmails.push(el);
|
|
}
|
|
});
|
|
|
|
console.log(`Found ${elementsWithEmails.length} elements with email addresses`);
|
|
|
|
// For each email element, look for nearby content
|
|
elementsWithEmails.forEach(emailEl => {
|
|
const email = emailEl.textContent.match(emailRegex)[0];
|
|
let messageContent = '';
|
|
let timestamp = '';
|
|
|
|
// Look for timestamp nearby
|
|
let current = emailEl;
|
|
for (let i = 0; i < 5; i++) {
|
|
current = current.nextElementSibling || current.parentElement?.nextElementSibling;
|
|
if (current && this.looksLikeTimestamp(current.textContent)) {
|
|
timestamp = current.textContent.trim();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Look for message content (usually after email/timestamp)
|
|
current = emailEl.parentElement;
|
|
|
|
// Add null check for parent
|
|
if (!current) {
|
|
console.warn('emailEl has no parent element');
|
|
return; // Skip this email element (return instead of continue in forEach)
|
|
}
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
if (!current) break; // Stop if current becomes null
|
|
|
|
const next = current.nextElementSibling;
|
|
if (next && next.textContent && next.textContent.length > 50 && !next.textContent.includes('@')) {
|
|
messageContent = next.textContent.trim();
|
|
break;
|
|
}
|
|
current = next || (current.parentElement ? current.parentElement : null);
|
|
}
|
|
|
|
if (messageContent) {
|
|
messages.push({
|
|
sender: email,
|
|
senderName: email.split('@')[0],
|
|
timestamp: timestamp || new Date().toLocaleString(),
|
|
content: messageContent,
|
|
subject: this.extractSubject() || 'No Subject',
|
|
isReply: messages.length > 0
|
|
});
|
|
}
|
|
});
|
|
|
|
return messages;
|
|
},
|
|
|
|
// Extract by finding timestamps first
|
|
extractByTimestamps() {
|
|
const messages = [];
|
|
const timestampPatterns = [
|
|
/\d{1,2}\/\d{1,2}\/\d{2,4}/,
|
|
/\d{4}-\d{2}-\d{2}/,
|
|
/\w+ \d{1,2}, \d{4}/,
|
|
/\d{1,2}:\d{2}\s*[AP]M/i
|
|
];
|
|
|
|
const elementsWithTimestamps = [];
|
|
document.querySelectorAll('*').forEach(el => {
|
|
const text = el.textContent;
|
|
if (text && timestampPatterns.some(pattern => pattern.test(text)) && el.children.length === 0) {
|
|
elementsWithTimestamps.push(el);
|
|
}
|
|
});
|
|
|
|
console.log(`Found ${elementsWithTimestamps.length} timestamp elements`);
|
|
|
|
elementsWithTimestamps.forEach(timeEl => {
|
|
const timestamp = timeEl.textContent.trim();
|
|
let sender = '';
|
|
let content = '';
|
|
|
|
// Look for email before or after timestamp
|
|
const parent = timeEl.parentElement;
|
|
|
|
// Add null check for parent
|
|
if (!parent) {
|
|
console.warn('timeEl has no parent element');
|
|
return; // Skip this timestamp element
|
|
}
|
|
|
|
const siblings = Array.from(parent.children);
|
|
const timeIndex = siblings.indexOf(timeEl);
|
|
|
|
// Check previous siblings for email
|
|
for (let i = timeIndex - 1; i >= 0 && i >= timeIndex - 3; i--) {
|
|
if (siblings[i] && siblings[i].textContent) {
|
|
const text = siblings[i].textContent;
|
|
const emailMatch = text.match(/[\w.+-]+@[\w.-]+\.\w+/);
|
|
if (emailMatch) {
|
|
sender = emailMatch[0];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check following siblings for content
|
|
for (let i = timeIndex + 1; i < siblings.length && i <= timeIndex + 3; i++) {
|
|
if (siblings[i] && siblings[i].textContent) {
|
|
const text = siblings[i].textContent;
|
|
if (text.length > 50 && !text.includes('@')) {
|
|
content = text.trim();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sender && content) {
|
|
messages.push({
|
|
sender,
|
|
senderName: sender.split('@')[0],
|
|
timestamp,
|
|
content,
|
|
subject: this.extractSubject() || 'No Subject',
|
|
isReply: messages.length > 0
|
|
});
|
|
}
|
|
});
|
|
|
|
return messages;
|
|
},
|
|
|
|
// Parse visible text with more patterns
|
|
parseVisibleText() {
|
|
const messages = [];
|
|
const bodyText = document.body.innerText;
|
|
|
|
// Multiple email patterns to try
|
|
const patterns = [
|
|
// Pattern 1: "From: ... Date: ... Subject: ..."
|
|
/(?:From|De|Von):\s*([^\n]+)\n(?:.*\n)*?(?:Date|Fecha|Datum):\s*([^\n]+)\n(?:.*\n)*?(?:Subject|Asunto|Betreff):\s*([^\n]+)\n([\s\S]+?)(?=(?:From|De|Von):|$)/gi,
|
|
|
|
// Pattern 2: "email@domain.com wrote:"
|
|
/([\w.+-]+@[\w.-]+\.\w+)\s+wrote:\s*([\s\S]+?)(?=[\w.+-]+@[\w.-]+\.\w+\s+wrote:|$)/gi,
|
|
|
|
// Pattern 3: "On [date], [email] wrote:"
|
|
/On\s+([^,]+),\s*([\w.+-]+@[\w.-]+\.\w+)\s+wrote:\s*([\s\S]+?)(?=On\s+[^,]+,|$)/gi,
|
|
|
|
// Pattern 4: Email followed by timestamp and content
|
|
/([\w.+-]+@[\w.-]+\.\w+)\s*\n\s*(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}[^\n]*)\s*\n\s*([\s\S]+?)(?=[\w.+-]+@[\w.-]+\.\w+|$)/gi,
|
|
|
|
// Pattern 5: Plusvibe specific - looking for "From:", "To:", "Subject:" pattern
|
|
/From:\s*([\w.+-]+@[\w.-]+\.\w+)[^\n]*\nTo:\s*([^\n]+)\n(?:Cc:\s*[^\n]+\n)?Subject:\s*([^\n]+)\n+([\s\S]+?)(?=From:|$)/gi
|
|
];
|
|
|
|
// Also try to extract from the visible format in Plusvibe
|
|
// Based on the screenshot, messages might just be separated by line breaks
|
|
const lines = bodyText.split('\n').map(l => l.trim()).filter(l => l);
|
|
let currentMessage = null;
|
|
let captureContent = false;
|
|
|
|
lines.forEach((line, i) => {
|
|
// Check if this line contains "From:" followed by an email
|
|
if (line.startsWith('From:') && line.includes('@')) {
|
|
// Save previous message
|
|
if (currentMessage && currentMessage.content) {
|
|
messages.push(currentMessage);
|
|
}
|
|
|
|
// Start new message
|
|
const emailMatch = line.match(/[\w.+-]+@[\w.-]+\.\w+/);
|
|
currentMessage = {
|
|
sender: emailMatch ? emailMatch[0] : 'unknown@email.com',
|
|
senderName: emailMatch ? emailMatch[0].split('@')[0] : 'Unknown',
|
|
timestamp: new Date().toLocaleString(),
|
|
subject: 'No Subject',
|
|
content: '',
|
|
isReply: messages.length > 0
|
|
};
|
|
captureContent = false;
|
|
}
|
|
// Check for "To:" line
|
|
else if (currentMessage && line.startsWith('To:')) {
|
|
currentMessage.recipient = line.replace('To:', '').trim();
|
|
}
|
|
// Check for "Subject:" line
|
|
else if (currentMessage && line.startsWith('Subject:')) {
|
|
currentMessage.subject = line.replace('Subject:', '').trim();
|
|
captureContent = true; // Start capturing content after subject
|
|
}
|
|
// Check for "Cc:" line
|
|
else if (currentMessage && line.startsWith('Cc:')) {
|
|
currentMessage.cc = line.replace('Cc:', '').trim();
|
|
}
|
|
// Capture content
|
|
else if (currentMessage && captureContent && line.length > 0) {
|
|
// Skip UI elements and buttons
|
|
if (!line.includes('Context-Specific Prompt') &&
|
|
!line.includes('Auto-Draft with GPT') &&
|
|
!line.includes('Cannot read properties')) {
|
|
currentMessage.content += (currentMessage.content ? '\n' : '') + line;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Add last message
|
|
if (currentMessage && currentMessage.content) {
|
|
messages.push(currentMessage);
|
|
}
|
|
|
|
// Try the regex patterns as backup
|
|
if (messages.length === 0) {
|
|
patterns.forEach((pattern, patternIndex) => {
|
|
let match;
|
|
while ((match = pattern.exec(bodyText)) !== null) {
|
|
let message = {};
|
|
|
|
switch(patternIndex) {
|
|
case 0: // From/Date/Subject pattern
|
|
message = {
|
|
sender: match[1].trim(),
|
|
senderName: match[1].split('<')[0].trim(),
|
|
timestamp: match[2].trim(),
|
|
subject: match[3].trim(),
|
|
content: match[4].trim(),
|
|
isReply: messages.length > 0
|
|
};
|
|
break;
|
|
|
|
case 1: // email wrote: pattern
|
|
message = {
|
|
sender: match[1],
|
|
senderName: match[1].split('@')[0],
|
|
timestamp: new Date().toLocaleString(),
|
|
subject: this.extractSubject() || 'No Subject',
|
|
content: match[2].trim(),
|
|
isReply: messages.length > 0
|
|
};
|
|
break;
|
|
|
|
case 2: // On date, email wrote:
|
|
message = {
|
|
sender: match[2],
|
|
senderName: match[2].split('@')[0],
|
|
timestamp: match[1],
|
|
subject: this.extractSubject() || 'No Subject',
|
|
content: match[3].trim(),
|
|
isReply: messages.length > 0
|
|
};
|
|
break;
|
|
|
|
case 3: // Email, timestamp, content
|
|
message = {
|
|
sender: match[1],
|
|
senderName: match[1].split('@')[0],
|
|
timestamp: match[2],
|
|
subject: this.extractSubject() || 'No Subject',
|
|
content: match[3].trim(),
|
|
isReply: messages.length > 0
|
|
};
|
|
break;
|
|
|
|
case 4: // Plusvibe From/To/Subject pattern
|
|
message = {
|
|
sender: match[1],
|
|
senderName: match[1].split('@')[0],
|
|
recipient: match[2],
|
|
subject: match[3],
|
|
content: match[4].trim(),
|
|
timestamp: new Date().toLocaleString(),
|
|
isReply: messages.length > 0
|
|
};
|
|
break;
|
|
}
|
|
|
|
if (message.content && message.content.length > 20) {
|
|
messages.push(message);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
console.log(`Parsed ${messages.length} messages from visible text`);
|
|
return messages;
|
|
},
|
|
|
|
// Extract from reply context (near the reply box)
|
|
extractFromReplyContext() {
|
|
const messages = [];
|
|
const replyBox = this.findReplyBox();
|
|
|
|
if (!replyBox) return messages;
|
|
|
|
// Navigate up the DOM tree to find the conversation container
|
|
let current = replyBox.parentElement;
|
|
let maxLevels = 10;
|
|
|
|
while (current && maxLevels > 0) {
|
|
// Check if this container has email content
|
|
const text = current.textContent;
|
|
if (text.includes('@') && text.length > 200) {
|
|
// This might be our conversation container
|
|
console.log('Found potential conversation container:', {
|
|
tagName: current.tagName,
|
|
className: current.className,
|
|
textLength: text.length
|
|
});
|
|
|
|
// Try to extract messages from this container
|
|
const containerMessages = this.extractFromContainer(current);
|
|
if (containerMessages.length > 0) {
|
|
return containerMessages;
|
|
}
|
|
}
|
|
|
|
current = current.parentElement;
|
|
maxLevels--;
|
|
}
|
|
|
|
return messages;
|
|
},
|
|
|
|
// Extract messages from a specific container
|
|
extractFromContainer(container) {
|
|
const messages = [];
|
|
|
|
// Strategy 1: Look for child elements with consistent structure
|
|
const children = Array.from(container.children);
|
|
children.forEach(child => {
|
|
const text = child.textContent;
|
|
if (text.includes('@') && text.length > 50) {
|
|
const emailMatch = text.match(/[\w.+-]+@[\w.-]+\.\w+/);
|
|
if (emailMatch) {
|
|
messages.push({
|
|
sender: emailMatch[0],
|
|
senderName: emailMatch[0].split('@')[0],
|
|
timestamp: this.extractTimestampFromText(text),
|
|
content: this.extractContentFromText(text, emailMatch[0]),
|
|
subject: this.extractSubject() || 'No Subject',
|
|
isReply: messages.length > 0
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return messages;
|
|
},
|
|
|
|
// Helper functions
|
|
looksLikeTimestamp(text) {
|
|
const patterns = [
|
|
/\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/,
|
|
/\d{4}-\d{2}-\d{2}/,
|
|
/\w+ \d{1,2}, \d{4}/,
|
|
/\d{1,2}:\d{2}\s*[AP]M/i,
|
|
/\d+ (hours?|days?|weeks?) ago/i
|
|
];
|
|
|
|
return patterns.some(pattern => pattern.test(text));
|
|
},
|
|
|
|
extractTimestampFromText(text) {
|
|
const patterns = [
|
|
/(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}(?:\s+\d{1,2}:\d{2}\s*[AP]M)?)/i,
|
|
/(\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?)/,
|
|
/(\w+ \d{1,2}, \d{4}(?:\s+at\s+\d{1,2}:\d{2}\s*[AP]M)?)/i,
|
|
/(\d+ (?:hours?|days?|weeks?) ago)/i
|
|
];
|
|
|
|
for (const pattern of patterns) {
|
|
const match = text.match(pattern);
|
|
if (match) return match[1];
|
|
}
|
|
|
|
return new Date().toLocaleString();
|
|
},
|
|
|
|
extractContentFromText(text, senderEmail) {
|
|
// Remove the sender email and clean up
|
|
let content = text.replace(senderEmail, '').trim();
|
|
|
|
// Remove common patterns
|
|
content = content.replace(/wrote:?\s*/i, '');
|
|
content = content.replace(/^on\s+.+?,\s*/i, '');
|
|
|
|
// Remove timestamp if at the beginning
|
|
const timestampPatterns = [
|
|
/^\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}[^\n]*\n*/,
|
|
/^\w+ \d{1,2}, \d{4}[^\n]*\n*/
|
|
];
|
|
|
|
timestampPatterns.forEach(pattern => {
|
|
content = content.replace(pattern, '');
|
|
});
|
|
|
|
return content.trim();
|
|
},
|
|
|
|
// Extract sender from element
|
|
extractSender(element) {
|
|
// Add null check
|
|
if (!element) {
|
|
console.warn('extractSender called with null/undefined element');
|
|
return 'unknown@email.com';
|
|
}
|
|
|
|
const selectors = [
|
|
'.sender-email',
|
|
'.from-email',
|
|
'.email-from',
|
|
'[data-sender]',
|
|
'.message-sender',
|
|
'.from'
|
|
];
|
|
|
|
for (const selector of selectors) {
|
|
const el = element.querySelector(selector);
|
|
if (el) {
|
|
return el.textContent.trim() || el.getAttribute('data-sender') || el.getAttribute('data-email');
|
|
}
|
|
}
|
|
|
|
// Look for email pattern in text
|
|
const text = element.textContent;
|
|
const emailMatch = text.match(/[\w.+-]+@[\w.-]+\.\w+/);
|
|
return emailMatch ? emailMatch[0] : 'unknown@email.com';
|
|
},
|
|
|
|
// Extract sender name
|
|
extractSenderName(element) {
|
|
// Add null check
|
|
if (!element) {
|
|
console.warn('extractSenderName called with null/undefined element');
|
|
return 'Unknown';
|
|
}
|
|
|
|
const selectors = [
|
|
'.sender-name',
|
|
'.from-name',
|
|
'[data-sender-name]',
|
|
'.message-author',
|
|
'.author'
|
|
];
|
|
|
|
for (const selector of selectors) {
|
|
const el = element.querySelector(selector);
|
|
if (el) {
|
|
return el.textContent.trim() || el.getAttribute('data-name');
|
|
}
|
|
}
|
|
|
|
// Fallback to sender email username
|
|
const sender = this.extractSender(element);
|
|
return sender.split('@')[0];
|
|
},
|
|
|
|
// Extract timestamp
|
|
extractTimestamp(element) {
|
|
// Add null check
|
|
if (!element) {
|
|
console.warn('extractTimestamp called with null/undefined element');
|
|
return new Date().toLocaleString();
|
|
}
|
|
|
|
const selectors = [
|
|
'.timestamp',
|
|
'.message-time',
|
|
'.sent-time',
|
|
'time',
|
|
'[data-timestamp]',
|
|
'.date'
|
|
];
|
|
|
|
for (const selector of selectors) {
|
|
const el = element.querySelector(selector);
|
|
if (el) {
|
|
return el.textContent.trim() ||
|
|
el.getAttribute('datetime') ||
|
|
el.getAttribute('data-timestamp') ||
|
|
el.getAttribute('title');
|
|
}
|
|
}
|
|
|
|
return new Date().toLocaleString();
|
|
},
|
|
|
|
// Extract message content
|
|
extractContent(element) {
|
|
// Add null check
|
|
if (!element) {
|
|
console.warn('extractContent called with null/undefined element');
|
|
return '';
|
|
}
|
|
|
|
const selectors = [
|
|
'.message-content',
|
|
'.message-body',
|
|
'.email-body',
|
|
'.content',
|
|
'[data-content]',
|
|
'.body'
|
|
];
|
|
|
|
for (const selector of selectors) {
|
|
const el = element.querySelector(selector);
|
|
if (el) {
|
|
const clone = el.cloneNode(true);
|
|
// Remove quoted text
|
|
const quotes = clone.querySelectorAll('.quoted-text, blockquote, .gmail_quote');
|
|
quotes.forEach(q => q.remove());
|
|
return clone.textContent.trim();
|
|
}
|
|
}
|
|
|
|
return element.textContent.trim();
|
|
},
|
|
|
|
// Extract subject
|
|
extractSubject(element) {
|
|
// Add null check - note: element parameter might be optional here
|
|
const selectors = [
|
|
'.subject',
|
|
'.email-subject',
|
|
'.message-subject',
|
|
'[data-subject]',
|
|
'.thread-subject',
|
|
'.conversation-subject',
|
|
'h1[class*="subject"]',
|
|
'h2[class*="subject"]',
|
|
'[aria-label*="subject"]'
|
|
];
|
|
|
|
// If element is provided, check it first
|
|
if (element) {
|
|
for (const selector of selectors) {
|
|
const el = element.querySelector(selector);
|
|
if (el) {
|
|
return el.textContent.trim() || el.getAttribute('data-subject');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check page header (no element needed)
|
|
const pageSubject = document.querySelector('h1.subject, h2.subject, .page-subject, .thread-title, .conversation-title');
|
|
if (pageSubject) {
|
|
return pageSubject.textContent.trim();
|
|
}
|
|
|
|
// Try to extract from page title
|
|
if (document.title && document.title.includes(' - ')) {
|
|
const titleParts = document.title.split(' - ');
|
|
if (titleParts.length > 1) {
|
|
// Return the first part that looks like a subject
|
|
return titleParts[0].trim();
|
|
}
|
|
}
|
|
|
|
// Look for "Re:" pattern in visible text
|
|
const rePattern = /Re:\s*([^\n]{10,100})/i;
|
|
const pageText = document.body.innerText;
|
|
const reMatch = pageText.match(rePattern);
|
|
if (reMatch) {
|
|
return reMatch[0].trim();
|
|
}
|
|
|
|
return null; // Return null to indicate no subject found
|
|
},
|
|
|
|
// Find the reply/compose box
|
|
findReplyBox() {
|
|
console.log('=== FINDING PLUSVIBE REPLY BOX ===');
|
|
|
|
const selectors = [
|
|
// MUI (Material-UI) specific selectors - PRIORITIZED
|
|
'textarea.MuiInputBase-input',
|
|
'textarea.MuiOutlinedInput-input',
|
|
'textarea.MuiInputBase-inputMultiline',
|
|
'.MuiInputBase-input',
|
|
'.MuiOutlinedInput-input',
|
|
|
|
// Common contenteditable patterns
|
|
'div[contenteditable="true"]',
|
|
'[contenteditable="true"]',
|
|
|
|
// Textarea patterns
|
|
'textarea',
|
|
'textarea.reply-input',
|
|
'textarea.compose-input',
|
|
'textarea[name="message"]',
|
|
'textarea[name="reply"]',
|
|
|
|
// Class-based selectors
|
|
'.reply-box',
|
|
'.compose-box',
|
|
'.message-input',
|
|
'.reply-input',
|
|
'.compose-input',
|
|
'.editor',
|
|
'.text-editor',
|
|
|
|
// Plusvibe specific guesses
|
|
'.pipl-reply-box',
|
|
'.plusvibe-compose',
|
|
'.unibox-reply',
|
|
|
|
// Role-based
|
|
'[role="textbox"]',
|
|
|
|
// Data attributes
|
|
'[data-reply-box]',
|
|
'[data-compose]',
|
|
'[data-message-input]'
|
|
];
|
|
|
|
for (const selector of selectors) {
|
|
const elements = document.querySelectorAll(selector);
|
|
for (const element of elements) {
|
|
if (element.offsetParent !== null) { // Check if visible
|
|
const rect = element.getBoundingClientRect();
|
|
if (rect.width > 100 && rect.height > 50) {
|
|
console.log('Found Plusvibe reply box:', selector);
|
|
return element;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
console.warn('Could not find Plusvibe reply box');
|
|
return null;
|
|
},
|
|
|
|
// Insert draft into reply box
|
|
insertDraft(draft) {
|
|
const replyBox = this.findReplyBox();
|
|
if (!replyBox) {
|
|
console.error('No reply box found to insert draft');
|
|
return false;
|
|
}
|
|
|
|
if (replyBox.tagName === 'TEXTAREA' || replyBox.tagName === 'INPUT') {
|
|
replyBox.value = draft;
|
|
replyBox.dispatchEvent(new Event('input', { bubbles: true }));
|
|
replyBox.dispatchEvent(new Event('change', { bubbles: true }));
|
|
} else if (replyBox.contentEditable === 'true') {
|
|
replyBox.innerHTML = draft.replace(/\n/g, '<br>');
|
|
replyBox.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
|
// Trigger any framework-specific events
|
|
const inputEvent = new InputEvent('input', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
inputType: 'insertText',
|
|
data: draft
|
|
});
|
|
replyBox.dispatchEvent(inputEvent);
|
|
}
|
|
|
|
console.log('Draft inserted into Plusvibe reply box');
|
|
return true;
|
|
},
|
|
|
|
// Get selected threads (for batch operations)
|
|
getSelectedThreads() {
|
|
const selected = [];
|
|
|
|
const selectors = [
|
|
'input[type="checkbox"]:checked',
|
|
'.thread-item.selected',
|
|
'.conversation-item.selected',
|
|
'[data-selected="true"]',
|
|
'.inbox-item.checked'
|
|
];
|
|
|
|
for (const selector of selectors) {
|
|
const elements = document.querySelectorAll(selector);
|
|
elements.forEach(element => {
|
|
const threadEl = element.closest('.thread-item, .conversation-item, .inbox-item');
|
|
if (threadEl) {
|
|
const threadInfo = {
|
|
id: threadEl.getAttribute('data-thread-id') ||
|
|
threadEl.getAttribute('data-conversation-id') ||
|
|
threadEl.id,
|
|
subject: threadEl.querySelector('.subject')?.textContent?.trim(),
|
|
sender: threadEl.querySelector('.sender, .from')?.textContent?.trim()
|
|
};
|
|
|
|
if (threadInfo.id) {
|
|
selected.push(threadInfo);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return selected;
|
|
},
|
|
|
|
// Diagnostic function to analyze DOM structure
|
|
analyzeDOMStructure() {
|
|
console.log('=== PLUSVIBE DOM ANALYSIS ===');
|
|
|
|
// 1. Find all elements with email addresses
|
|
const allElements = document.querySelectorAll('*');
|
|
const emailElements = [];
|
|
|
|
allElements.forEach(el => {
|
|
if (el.textContent && el.textContent.match(/[\w.+-]+@[\w.-]+\.\w+/)) {
|
|
emailElements.push({
|
|
tagName: el.tagName,
|
|
className: el.className,
|
|
id: el.id,
|
|
text: el.textContent.substring(0, 100),
|
|
attributes: Array.from(el.attributes).map(attr => `${attr.name}="${attr.value}"`)
|
|
});
|
|
}
|
|
});
|
|
|
|
console.log('Elements containing email addresses:', emailElements);
|
|
|
|
// 2. Look for conversation/message containers
|
|
const possibleContainers = [
|
|
// Common patterns
|
|
'div[class*="message"]',
|
|
'div[class*="email"]',
|
|
'div[class*="thread"]',
|
|
'div[class*="conversation"]',
|
|
'div[class*="mail"]',
|
|
'div[class*="inbox"]',
|
|
'article',
|
|
'section[class*="message"]',
|
|
// Check for list items
|
|
'li[class*="message"]',
|
|
'li[class*="email"]',
|
|
// Data attributes
|
|
'[data-message]',
|
|
'[data-email]',
|
|
'[data-thread]',
|
|
'[data-conversation]'
|
|
];
|
|
|
|
possibleContainers.forEach(selector => {
|
|
const elements = document.querySelectorAll(selector);
|
|
if (elements.length > 0) {
|
|
console.log(`Found ${elements.length} elements matching "${selector}":`);
|
|
elements.forEach((el, i) => {
|
|
if (i < 3) { // Log first 3
|
|
console.log({
|
|
selector,
|
|
className: el.className,
|
|
id: el.id,
|
|
textLength: el.textContent.length,
|
|
hasEmail: el.textContent.includes('@'),
|
|
preview: el.textContent.substring(0, 100) + '...'
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// 3. Analyze the reply/compose area
|
|
console.log('\n=== REPLY AREA ANALYSIS ===');
|
|
const replyArea = this.findReplyBox();
|
|
if (replyArea) {
|
|
// Look for nearby elements that might contain the thread
|
|
const parent = replyArea.parentElement;
|
|
const grandparent = parent?.parentElement;
|
|
|
|
console.log('Reply box parent:', {
|
|
tagName: parent?.tagName,
|
|
className: parent?.className,
|
|
id: parent?.id
|
|
});
|
|
|
|
console.log('Reply box grandparent:', {
|
|
tagName: grandparent?.tagName,
|
|
className: grandparent?.className,
|
|
id: grandparent?.id
|
|
});
|
|
|
|
// Look for siblings that might contain messages
|
|
const siblings = parent ? Array.from(parent.children) : [];
|
|
siblings.forEach((sibling, i) => {
|
|
if (sibling !== replyArea && sibling.textContent.includes('@')) {
|
|
console.log(`Sibling ${i} (potential message container):`, {
|
|
tagName: sibling.tagName,
|
|
className: sibling.className,
|
|
textLength: sibling.textContent.length,
|
|
preview: sibling.textContent.substring(0, 100) + '...'
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// 4. Check for iframe content
|
|
const iframes = document.querySelectorAll('iframe');
|
|
console.log(`\nFound ${iframes.length} iframes`);
|
|
iframes.forEach((iframe, i) => {
|
|
try {
|
|
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
|
if (iframeDoc && iframeDoc.body.textContent.includes('@')) {
|
|
console.log(`Iframe ${i} contains email content`);
|
|
}
|
|
} catch (e) {
|
|
console.log(`Iframe ${i} is cross-origin`);
|
|
}
|
|
});
|
|
|
|
console.log('=== END DOM ANALYSIS ===');
|
|
},
|
|
|
|
// New helper function to find messages by searching for email content
|
|
findMessagesByContent() {
|
|
console.log('=== FINDING MESSAGES BY CONTENT ===');
|
|
|
|
// Find email addresses dynamically from the current page
|
|
const emailRegex = /[\w.+-]+@[\w.-]+\.\w+/g;
|
|
const pageText = document.body.innerText;
|
|
const foundEmails = new Set();
|
|
let match;
|
|
|
|
while ((match = emailRegex.exec(pageText)) !== null) {
|
|
foundEmails.add(match[0]);
|
|
}
|
|
|
|
console.log('Found emails on page:', Array.from(foundEmails));
|
|
|
|
// Find all elements containing email addresses
|
|
const potentialContainers = [];
|
|
const allElements = document.querySelectorAll('*');
|
|
|
|
allElements.forEach(el => {
|
|
const text = el.textContent;
|
|
if (text && text.match(emailRegex)) {
|
|
// Check if this element has reasonable size and isn't too big
|
|
const childCount = el.children.length;
|
|
const textLength = text.length;
|
|
|
|
if (childCount < 50 && textLength > 50 && textLength < 5000) {
|
|
potentialContainers.push({
|
|
element: el,
|
|
tagName: el.tagName,
|
|
className: el.className,
|
|
id: el.id,
|
|
childCount: childCount,
|
|
textLength: textLength,
|
|
preview: text.substring(0, 200)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log(`Found ${potentialContainers.length} potential message containers`);
|
|
potentialContainers.forEach((container, i) => {
|
|
if (i < 5) { // Log only first 5 to avoid spam
|
|
console.log(`Container ${i + 1}:`, container);
|
|
}
|
|
});
|
|
|
|
// Try to identify the pattern
|
|
const messageElements = [];
|
|
potentialContainers.forEach(container => {
|
|
// Look for elements that seem to be individual messages
|
|
const parent = container.element.parentElement;
|
|
const siblings = parent ? Array.from(parent.children) : [];
|
|
|
|
// Check if siblings have similar structure (likely messages)
|
|
const similarSiblings = siblings.filter(sib =>
|
|
sib.tagName === container.element.tagName &&
|
|
sib.className === container.element.className
|
|
);
|
|
|
|
if (similarSiblings.length > 1) {
|
|
console.log('Found message pattern:', {
|
|
selector: `${container.element.tagName}.${container.element.className.split(' ').join('.')}`,
|
|
count: similarSiblings.length
|
|
});
|
|
|
|
similarSiblings.forEach(msg => {
|
|
messageElements.push(msg);
|
|
});
|
|
}
|
|
});
|
|
|
|
return messageElements;
|
|
},
|
|
|
|
|
|
// New method: Extract from MUI/React structures
|
|
extractFromMUIStructure() {
|
|
console.log('=== EXTRACTING FROM MUI STRUCTURE ===');
|
|
const messages = [];
|
|
|
|
// Look for MUI Box components that might contain messages
|
|
const muiSelectors = [
|
|
'.MuiBox-root'
|
|
// '[class*="MuiBox"]',
|
|
// '[class*="css-"][class*="root"]',
|
|
// 'div[class^="css-"]',
|
|
];
|
|
|
|
const replyBox = this.findReplyBox();
|
|
const replyContainer = replyBox.closest('div');
|
|
|
|
// Find all potential message containers
|
|
const potentialMessages = [];
|
|
muiSelectors.forEach(selector => {
|
|
const elements = document.querySelectorAll(selector);
|
|
elements.forEach(el => {
|
|
|
|
const boxRect = el.getBoundingClientRect();
|
|
const replyRect = replyContainer.getBoundingClientRect();
|
|
if (boxRect.bottom < replyRect.top) {
|
|
const text = el.textContent;
|
|
// Check if this element looks like a message (has email and substantial content)
|
|
if (text && text.includes('@') && text.length > 100 && text.length < 5000) {
|
|
// Check it's not too nested (avoid getting parent containers)
|
|
const emailCount = (text.match(/@/g) || []).length;
|
|
if (emailCount <= 3) { // Reasonable number of email addresses
|
|
potentialMessages.push({
|
|
element: el,
|
|
text: text,
|
|
emailCount: emailCount,
|
|
childCount: el.children.length
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
console.log(`Found ${potentialMessages.length} potential message containers`);
|
|
|
|
// Sort by likelihood of being a single message (not too many children, not too much text)
|
|
potentialMessages.sort((a, b) => b.top - a.top);
|
|
|
|
/* potentialMessages.sort((a, b) => {
|
|
const aScore = Math.abs(500 - a.text.length) + (a.childCount * 10);
|
|
const bScore = Math.abs(500 - b.text.length) + (b.childCount * 10);
|
|
return aScore - bScore;
|
|
});
|
|
*/
|
|
|
|
// Process the most likely candidates
|
|
const processedElements = new Set();
|
|
potentialMessages.forEach(candidate => {
|
|
// Skip if we've already processed this element or its parent
|
|
if (processedElements.has(candidate.element)) return;
|
|
|
|
let parent = candidate.element.parentElement;
|
|
while (parent) {
|
|
if (processedElements.has(parent)) return;
|
|
parent = parent.parentElement;
|
|
}
|
|
|
|
processedElements.add(candidate.element);
|
|
|
|
const text = candidate.text;
|
|
const emailMatch = text.match(/[\w.+-]+@[\w.-]+\.\w+/);
|
|
|
|
if (emailMatch) {
|
|
// Extract message details
|
|
const message = {
|
|
sender: emailMatch[0],
|
|
senderName: this.extractSenderNameFromEmail(emailMatch[0]),
|
|
timestamp: this.extractTimestampFromText(text),
|
|
content: this.cleanMessageContent(text, emailMatch[0]),
|
|
subject: this.extractSubject() || 'No Subject',
|
|
isReply: messages.length > 0
|
|
};
|
|
|
|
if (message.content && message.content.length > 20) {
|
|
console.log('Extracted message from MUI structure:', {
|
|
sender: message.sender,
|
|
contentLength: message.content.length,
|
|
preview: message.content.substring(0, 100) + '...'
|
|
});
|
|
messages.push(message);
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log(`Extracted ${messages.length} messages from MUI structure`);
|
|
return messages;
|
|
},
|
|
|
|
// Helper to clean message content more aggressively
|
|
cleanMessageContent(text, senderEmail) {
|
|
let content = text;
|
|
|
|
// Remove the sender email
|
|
content = content.replace(new RegExp(senderEmail.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '');
|
|
|
|
// Remove common UI elements and timestamps
|
|
const patternsToRemove = [
|
|
/Context-Specific Prompt:?/gi,
|
|
/Auto-Draft with GPT/gi,
|
|
/Cannot read properties/gi,
|
|
/\d{1,2}:\d{2}\s*[AP]M/gi,
|
|
/\d{1,2}\/\d{1,2}\/\d{2,4}/g,
|
|
/\w+,\s+\w+\s+\d{1,2},\s+\d{4}/gi,
|
|
/^From:.*$/gm,
|
|
/^To:.*$/gm,
|
|
/^Subject:.*$/gm,
|
|
/^Date:.*$/gm,
|
|
/^Cc:.*$/gm,
|
|
/^Re:\s*/gim
|
|
];
|
|
|
|
patternsToRemove.forEach(pattern => {
|
|
content = content.replace(pattern, '');
|
|
});
|
|
|
|
// Remove excessive whitespace
|
|
content = content
|
|
.split('\n')
|
|
.map(line => line.trim())
|
|
.filter(line => line.length > 0)
|
|
.join('\n');
|
|
|
|
// If content is too short after cleaning, it might be just UI elements
|
|
if (content.length < 20) return '';
|
|
|
|
return content.trim();
|
|
},
|
|
|
|
// Fallback with hardcoded content based on visible emails
|
|
getHardcodedFallback() {
|
|
console.log('Using hardcoded fallback - this should only be used for debugging');
|
|
// This method should not contain any specific email addresses or content
|
|
// It should return an empty array to force proper extraction methods
|
|
return [];
|
|
},
|
|
|
|
// NEW: Diagnostic function to find thread boundaries
|
|
findThreadBoundaries() {
|
|
console.log('=== FINDING THREAD BOUNDARIES ===');
|
|
|
|
// Look for the reply box first as an anchor point
|
|
const replyBox = this.findReplyBox();
|
|
if (!replyBox) {
|
|
console.error('No reply box found - cannot determine thread context');
|
|
return null;
|
|
}
|
|
|
|
console.log('Found reply box, traversing up to find thread container...');
|
|
|
|
// Get the current thread ID from URL
|
|
const currentThreadId = this.getCurrentThreadId();
|
|
console.log('Current thread ID from URL:', currentThreadId);
|
|
|
|
// Traverse up from reply box to find the thread container
|
|
let current = replyBox;
|
|
let threadContainer = null;
|
|
let previousEmailCount = 0;
|
|
let level = 0;
|
|
|
|
while (current && level < 15) {
|
|
const emailCount = (current.textContent.match(/@/g) || []).length;
|
|
const rect = current.getBoundingClientRect();
|
|
|
|
console.log(`Level ${level}:`, {
|
|
tagName: current.tagName,
|
|
className: current.className.substring(0, 100),
|
|
emailCount: emailCount,
|
|
dimensions: `${Math.round(rect.width)}x${Math.round(rect.height)}`
|
|
});
|
|
|
|
// CRITICAL: Stop when email count jumps significantly (indicates we've hit the main container)
|
|
if (previousEmailCount > 0 && emailCount > previousEmailCount * 10) {
|
|
console.log(`Email count jumped from ${previousEmailCount} to ${emailCount} - stopping here`);
|
|
break;
|
|
}
|
|
|
|
// Check if this looks like a good thread container
|
|
const hasReasonableEmails = emailCount >= 2 && emailCount <= 20; // Thread has 2-20 emails max
|
|
const isReasonableSize = rect.width > 300 && rect.width < window.innerWidth * 0.8;
|
|
const hasNoInboxElements = !current.querySelector('.inbox-item, input[type="checkbox"]');
|
|
const isMuiDrawer = current.className.includes('MuiRoot') ||
|
|
current.closest('.MuiBox-root, .MuiBox-root');
|
|
|
|
if (hasReasonableEmails && isReasonableSize && hasNoInboxElements) {
|
|
console.log('Found potential thread container at level', level);
|
|
threadContainer = current;
|
|
// Don't break - keep going to see if there's a better container
|
|
// But store this as our best candidate so far
|
|
}
|
|
|
|
// Special handling for MUI Drawer - this often contains just the thread
|
|
if (isMuiDrawer && emailCount >= 2 && emailCount <= 20) {
|
|
console.log('Found MUI Drawer container - likely thread panel');
|
|
threadContainer = current;
|
|
}
|
|
|
|
previousEmailCount = emailCount;
|
|
current = current.parentElement;
|
|
level++;
|
|
}
|
|
|
|
if (threadContainer) {
|
|
console.log('Thread container identified:', {
|
|
element: threadContainer,
|
|
className: threadContainer.className,
|
|
id: threadContainer.id,
|
|
finalEmailCount: (threadContainer.textContent.match(/@/g) || []).length
|
|
});
|
|
|
|
// Extract messages only from this container
|
|
return this.extractMessagesFromContainer(threadContainer);
|
|
}
|
|
|
|
console.error('Could not identify thread container');
|
|
return null;
|
|
},
|
|
|
|
// Extract messages from a specific container only
|
|
extractMessagesFromContainer(container) {
|
|
console.log('Extracting messages from specific container...');
|
|
const messages = [];
|
|
|
|
// Get visible email addresses to identify the current thread
|
|
const visibleEmails = new Set();
|
|
const emailRegex = /[\w.+-]+@[\w.-]+\.\w+/g;
|
|
let match;
|
|
while ((match = emailRegex.exec(container.textContent)) !== null) {
|
|
visibleEmails.add(match[0].toLowerCase());
|
|
}
|
|
console.log('Visible emails in container:', Array.from(visibleEmails));
|
|
|
|
// NEW: For Plusvibe, messages appear to be simple text blocks separated by line breaks
|
|
// Based on the DOM structure shown in the screenshots
|
|
const lines = container.innerText.split('\n').map(l => l.trim()).filter(l => l);
|
|
|
|
let currentMessage = null;
|
|
let captureMode = false;
|
|
let lastSender = null;
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
const nextLine = lines[i + 1] || '';
|
|
|
|
// Check if this line contains an email address (potential sender)
|
|
const emailMatch = line.match(/^([\w.+-]+@[\w.-]+\.\w+)$/);
|
|
if (emailMatch) {
|
|
// Save previous message if exists
|
|
if (currentMessage && currentMessage.content.trim()) {
|
|
messages.push(currentMessage);
|
|
}
|
|
|
|
// Start new message
|
|
lastSender = emailMatch[1];
|
|
currentMessage = {
|
|
sender: lastSender,
|
|
senderName: this.extractSenderNameFromEmail(lastSender),
|
|
timestamp: this.findNearbyTimestamp(lines, i) || new Date().toLocaleString(),
|
|
content: '',
|
|
subject: this.extractSubject() || 'No Subject',
|
|
isReply: messages.length > 0
|
|
};
|
|
captureMode = true;
|
|
continue;
|
|
}
|
|
|
|
// Check if this looks like a timestamp (e.g., "Jun 13, 2025, 08:06:43 PM PDT")
|
|
if (this.looksLikeTimestamp(line) && currentMessage) {
|
|
currentMessage.timestamp = line;
|
|
continue;
|
|
}
|
|
|
|
// Skip UI elements and system messages
|
|
if (line.includes('Context-Specific Prompt') ||
|
|
line.includes('Auto-Draft with GPT') ||
|
|
line.includes('wrote:') ||
|
|
line.length < 10) {
|
|
continue;
|
|
}
|
|
|
|
// Capture content
|
|
if (captureMode && currentMessage && line.length > 10) {
|
|
// Check if this line starts a new message (has an email)
|
|
if (line.includes('@') && !line.startsWith('Hi ') && !line.includes('wrote:')) {
|
|
// This might be the start of a new message, save current
|
|
if (currentMessage.content.trim()) {
|
|
messages.push(currentMessage);
|
|
currentMessage = null;
|
|
captureMode = false;
|
|
i--; // Re-process this line
|
|
continue;
|
|
}
|
|
}
|
|
|
|
currentMessage.content += (currentMessage.content ? '\n' : '') + line;
|
|
}
|
|
}
|
|
|
|
// Don't forget the last message
|
|
if (currentMessage && currentMessage.content.trim()) {
|
|
messages.push(currentMessage);
|
|
}
|
|
|
|
// If we still have no messages, try the original MUI extraction as fallback
|
|
if (messages.length === 0) {
|
|
console.log('Simple extraction failed, trying MUI component extraction...');
|
|
|
|
// Look for divs that might be individual messages
|
|
const messageSelectors = [
|
|
'div.MuiBox-root',
|
|
'div[class*="css-"][class*="MuiBox"]',
|
|
'div[class*="css-"]:not([class*="MuiDrawer"])'
|
|
];
|
|
|
|
for (const selector of messageSelectors) {
|
|
const elements = container.querySelectorAll(selector);
|
|
elements.forEach((el, index) => {
|
|
const text = el.textContent;
|
|
const elementEmails = (text.match(emailRegex) || []).map(e => e.toLowerCase());
|
|
|
|
// Check if this element contains exactly one of our thread emails
|
|
const matchingEmails = elementEmails.filter(e => visibleEmails.has(e));
|
|
if (matchingEmails.length === 1 && text.length > 50 && text.length < 3000) {
|
|
const message = {
|
|
sender: matchingEmails[0],
|
|
senderName: this.extractSenderNameFromEmail(matchingEmails[0]),
|
|
timestamp: this.extractTimestampFromText(text),
|
|
content: this.cleanMessageContent(text, matchingEmails[0]),
|
|
subject: this.extractSubject() || 'No Subject',
|
|
isReply: messages.length > 0
|
|
};
|
|
|
|
if (message.content && message.content.length > 20) {
|
|
messages.push(message);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
console.log(`Extracted ${messages.length} messages from container`);
|
|
return messages;
|
|
},
|
|
|
|
// Helper to find nearby timestamp
|
|
findNearbyTimestamp(lines, currentIndex) {
|
|
// Look within 3 lines before or after for a timestamp
|
|
for (let offset = -3; offset <= 3; offset++) {
|
|
const idx = currentIndex + offset;
|
|
if (idx >= 0 && idx < lines.length) {
|
|
if (this.looksLikeTimestamp(lines[idx])) {
|
|
return lines[idx];
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
// Helper to extract sender name from email
|
|
extractSenderNameFromEmail(email) {
|
|
// Clean up email and extract username
|
|
const username = email.split('@')[0];
|
|
|
|
// Capitalize first letter of each word and handle common separators
|
|
return username
|
|
.split(/[._-]/)
|
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
.join(' ');
|
|
}
|
|
};
|
|
|
|
// Initialize on load
|
|
PlusvibeExtractor.init();
|
|
|
|
// Make it globally available
|
|
window.PlusvibeExtractor = PlusvibeExtractor;
|
|
|
|
console.log('=== PLUSVIBE EXTRACTOR LOADED ===');
|
|
console.log('PlusvibeExtractor available at window.PlusvibeExtractor');
|
|
console.log('Current thread ID:', PlusvibeExtractor.getCurrentThreadId());
|
|
|
|
// Quick test function
|
|
window.testPlusvibeExtraction = async function() {
|
|
console.log('=== TESTING PLUSVIBE EXTRACTION ===');
|
|
|
|
// Test thread ID
|
|
//const threadId = PlusvibeExtractor.getCurrentThreadId();
|
|
//console.log('Thread ID:', threadId);
|
|
|
|
// Test MUI extraction specifically
|
|
console.log('\nTesting MUI extraction...');
|
|
const muiMessages = PlusvibeExtractor.extractFromMUIStructure();
|
|
console.log(`Found ${muiMessages.length} messages via MUI extraction`);
|
|
muiMessages.forEach((msg, i) => {
|
|
console.log(`\nMessage ${i + 1}:`);
|
|
console.log('From:', msg.sender);
|
|
console.log('Content preview:', msg.content.substring(0, 200) + '...');
|
|
});
|
|
|
|
// Test full extraction
|
|
console.log('\nTesting full extraction pipeline...');
|
|
const allMessages = await PlusvibeExtractor.extractThreadMessages();
|
|
console.log(`Total messages extracted: ${allMessages.length}`);
|
|
|
|
console.log('=== END TESTING ===');
|
|
return allMessages;
|
|
};
|
|
|
|
console.log('Run testPlusvibeExtraction() to test message extraction');
|
|
|
|
// Quick diagnostic function
|
|
window.quickDiagnosePlusvibe = function() {
|
|
console.log('=== QUICK PLUSVIBE DIAGNOSTIC ===');
|
|
|
|
// 1. Check thread ID
|
|
//const threadId = PlusvibeExtractor.getCurrentThreadId();
|
|
//console.log('Thread ID:', threadId);
|
|
|
|
// 2. Find reply box
|
|
const replyBox = PlusvibeExtractor.findReplyBox();
|
|
if (replyBox) {
|
|
console.log('Reply box found ✓');
|
|
|
|
// 3. Test thread boundary detection
|
|
console.log('\nTesting thread boundary detection...');
|
|
const boundedMessages = PlusvibeExtractor.findThreadBoundaries();
|
|
if (boundedMessages && boundedMessages.length > 0) {
|
|
console.log(`Found ${boundedMessages.length} messages in thread:`);
|
|
boundedMessages.forEach((msg, i) => {
|
|
console.log(`\nMessage ${i + 1}:`);
|
|
console.log(' From:', msg.senderName, `<${msg.sender}>`);
|
|
console.log(' Time:', msg.timestamp);
|
|
console.log(' Subject:', msg.subject);
|
|
console.log(' Preview:', msg.content.substring(0, 100) + '...');
|
|
});
|
|
|
|
// Generic validation - check if messages are from current thread
|
|
console.log('\nValidation:');
|
|
console.log(' Number of unique senders:', new Set(boundedMessages.map(m => m.sender)).size);
|
|
console.log(' Thread appears coherent:', boundedMessages.length >= 2 ? '✓' : '✗');
|
|
console.log(' Messages have content:', boundedMessages.every(m => m.content.length > 10) ? '✓' : '✗');
|
|
|
|
if (boundedMessages.length >= 2 && boundedMessages.every(m => m.content.length > 10)) {
|
|
console.log('\n✅ SUCCESS: Extracted thread messages successfully!');
|
|
} else {
|
|
console.log('\n❌ ERROR: Could not extract valid thread messages');
|
|
}
|
|
} else {
|
|
console.log('❌ No messages found');
|
|
}
|
|
} else {
|
|
console.log('❌ No reply box found');
|
|
}
|
|
|
|
console.log('\n=== END DIAGNOSTIC ===');
|
|
};
|
|
|
|
console.log('Run quickDiagnosePlusvibe() for quick diagnostic');
|