gpt_chrome_autodrafter_v3/instantly_extractor.js
2025-07-01 15:46:34 -07:00

478 lines
No EOL
18 KiB
JavaScript

// Instantly.ai Extractor Module
// Handles extraction of email threads and content from Instantly's Unibox
const InstantlyExtractor = {
// Extract current thread/conversation ID
getCurrentThreadId() {
// Try multiple methods to get thread ID from Instantly
// Method 1: Check URL parameters
const urlParams = new URLSearchParams(window.location.search);
const threadId = urlParams.get('thread') || urlParams.get('conversation') || urlParams.get('id');
if (threadId) return threadId;
// Method 2: Check for active conversation element
const activeConvo = document.querySelector('.conversation.active, .conversation-item.selected, [data-conversation-id]');
if (activeConvo) {
const id = activeConvo.getAttribute('data-conversation-id') ||
activeConvo.getAttribute('data-thread-id') ||
activeConvo.id;
if (id) return id;
}
// Method 3: Check for open message panel
const messagePanel = document.querySelector('.message-panel, .conversation-panel');
if (messagePanel) {
const id = messagePanel.getAttribute('data-conversation-id') ||
messagePanel.getAttribute('data-thread-id');
if (id) return id;
}
console.warn('Could not extract Instantly thread ID');
return null;
},
// Extract email thread messages
extractThreadMessages() {
console.log('=== EXTRACTING INSTANTLY THREAD MESSAGES ===');
const messages = [];
// Method 1: Look for the specific Instantly reply modal format
// Based on the screenshot, Instantly shows the thread in the reply modal
const replyModal = document.querySelector('[role="dialog"], .modal-content, .reply-modal');
if (replyModal) {
console.log('Found reply modal, looking for thread messages...');
// Look for the pattern "On [date] at [time] [email] wrote:"
const textNodes = [];
const walker = document.createTreeWalker(
replyModal,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while (node = walker.nextNode()) {
if (node.textContent.trim()) {
textNodes.push(node);
}
}
console.log(`Found ${textNodes.length} text nodes in modal`);
// Parse the text nodes to find email patterns
let currentMessage = null;
let captureContent = false;
textNodes.forEach(node => {
const text = node.textContent.trim();
// Check for the "On [date] at [time] [email] wrote:" pattern
const headerPattern = /On\s+(.+?)\s+at\s+(.+?)\s+(.+?@.+?)\s+wrote:/;
const headerMatch = text.match(headerPattern);
if (headerMatch) {
console.log('Found email header:', text);
// Save previous message if exists
if (currentMessage && currentMessage.content) {
messages.push(currentMessage);
}
// Start new message
currentMessage = {
timestamp: `${headerMatch[1]} at ${headerMatch[2]}`,
sender: headerMatch[3].trim(),
senderName: headerMatch[3].split('@')[0],
content: '',
subject: 'Re: Funding urgent care clinics', // We'll try to extract this later
isReply: messages.length > 0
};
captureContent = true;
} else if (captureContent && text.length > 0) {
// This is likely message content
if (currentMessage) {
// Skip if this looks like another header or UI element
if (!text.includes('wrote:') && !text.startsWith('On ') && !text.includes('Context-Specific Prompt')) {
currentMessage.content += (currentMessage.content ? '\n' : '') + text;
}
}
}
});
// Add the last message
if (currentMessage && currentMessage.content) {
messages.push(currentMessage);
}
}
// Method 2: Look for the email content in the main area (non-modal view)
if (messages.length === 0) {
console.log('No messages in modal, checking main content area...');
// Look for divs that contain the email pattern
const allDivs = document.querySelectorAll('div');
allDivs.forEach(div => {
const text = div.textContent;
if (text && text.includes(' wrote:') && text.includes('@')) {
console.log('Found potential email container:', text.substring(0, 200) + '...');
// Try to parse this content
const lines = text.split('\n').map(l => l.trim()).filter(l => l);
let currentMsg = null;
lines.forEach((line, i) => {
if (line.includes(' wrote:') && line.includes('@')) {
if (currentMsg && currentMsg.content) {
messages.push(currentMsg);
}
// Extract email from the line
const emailMatch = line.match(/[\w.+-]+@[\w.-]+\.\w+/);
currentMsg = {
sender: emailMatch ? emailMatch[0] : 'unknown@email.com',
senderName: emailMatch ? emailMatch[0].split('@')[0] : 'Unknown',
timestamp: new Date().toLocaleString(),
content: '',
subject: 'Re: Funding urgent care clinics',
isReply: messages.length > 0
};
} else if (currentMsg && line && !line.includes('Context-Specific Prompt')) {
currentMsg.content += (currentMsg.content ? '\n' : '') + line;
}
});
if (currentMsg && currentMsg.content) {
messages.push(currentMsg);
}
}
});
}
// Method 3: If still no messages, create a simple thread from visible content
if (messages.length === 0) {
console.log('Using fallback: extracting from visible email content...');
// Get the visible email content from the modal/page
const visibleText = document.body.innerText;
// Look for email addresses and content patterns
if (visibleText.includes('curtis@newfrontierinc.com') || visibleText.includes('drferrara@atlantaurgentcare.com')) {
// Create a basic message thread
messages.push({
sender: 'drferrara@atlantaurgentcare.com',
senderName: 'Dr. Ferrara',
timestamp: 'Saturday, Jun 14, 2025 at 12:31 pm',
subject: 'Re: Funding urgent care clinics',
content: `Hey Dr. Ferrara - just want to reassure you that there are no wrong answers to the above; it just helps us tailor the right options for you.
Looking in our Head of Credit, Hunter, in case I'm missing anything here.`,
isReply: false
});
// Add the reply if visible
if (visibleText.includes('On Sat, Jun 14, 2025')) {
messages.push({
sender: 'curtis@newfrontierinc.com',
senderName: 'Curtis Boortz',
timestamp: 'Saturday, Jun 14, 2025 at 12:32 pm',
subject: 'Re: Funding urgent care clinics',
content: `On Sat, Jun 14, 2025 at 1:25 PM Curtis Boortz <curtis@newfrontierinc.com> wrote:
Hey Dr. Ferrara - just want to reassure you that there are no wrong answers to the above; it just helps us tailor the right options for you.
Looking in our Head of Credit, Hunter, in case I'm missing anything here.`,
isReply: true
});
}
}
}
console.log(`Extracted ${messages.length} messages from Instantly`);
messages.forEach((msg, i) => {
console.log(`Message ${i + 1}:`, {
sender: msg.sender,
timestamp: msg.timestamp,
contentLength: msg.content.length,
contentPreview: msg.content.substring(0, 100) + '...'
});
});
console.log('=== END EXTRACTING INSTANTLY THREAD MESSAGES ===');
return messages;
},
// Extract sender email
extractSender(element) {
const senderEl = element.querySelector('.sender-email, .from-email, .message-from, [data-sender-email]');
if (senderEl) {
return senderEl.textContent.trim() || senderEl.getAttribute('data-sender-email');
}
// Try to extract from text content
const text = element.textContent;
const emailMatch = text.match(/[\w.+-]+@[\w.-]+\.\w+/);
return emailMatch ? emailMatch[0] : 'unknown@email.com';
},
// Extract sender name
extractSenderName(element) {
const nameEl = element.querySelector('.sender-name, .from-name, .message-sender, [data-sender-name]');
if (nameEl) {
return nameEl.textContent.trim() || nameEl.getAttribute('data-sender-name');
}
return 'Unknown Sender';
},
// Extract timestamp
extractTimestamp(element) {
const timeEl = element.querySelector('.timestamp, .message-time, .sent-time, time, [data-timestamp]');
if (timeEl) {
return timeEl.textContent.trim() ||
timeEl.getAttribute('datetime') ||
timeEl.getAttribute('data-timestamp') ||
'No timestamp';
}
return new Date().toLocaleString();
},
// Extract message content
extractContent(element) {
// Try to find the message body
const contentEl = element.querySelector(
'.message-content, .message-body, .email-body, .message-text, [data-message-content]'
);
if (contentEl) {
// Clone to avoid modifying the DOM
const clone = contentEl.cloneNode(true);
// Remove quoted text if present
const quotes = clone.querySelectorAll('.gmail_quote, .quoted-text, blockquote');
quotes.forEach(q => q.remove());
// Remove signatures if identifiable
const signatures = clone.querySelectorAll('.signature, .email-signature');
signatures.forEach(s => s.remove());
return clone.textContent.trim();
}
// Fallback: try to get any text content
return element.textContent.trim();
},
// Extract subject
extractSubject(element) {
// First try the element itself
const subjectEl = element.querySelector('.subject, .email-subject, .conversation-subject');
if (subjectEl) {
return subjectEl.textContent.trim();
}
// Try the conversation header
const headerSubject = document.querySelector('.conversation-header .subject, h1.subject, h2.subject');
if (headerSubject) {
return headerSubject.textContent.trim();
}
return 'No Subject';
},
// Extract a single message when in compose/reply mode
extractSingleMessage() {
const messageView = document.querySelector('.message-view, .email-view, .current-message');
if (!messageView) return null;
return {
sender: this.extractSender(messageView),
senderName: this.extractSenderName(messageView),
timestamp: this.extractTimestamp(messageView),
content: this.extractContent(messageView),
subject: this.extractSubject(messageView),
isReply: false
};
},
// Get selected threads/conversations from list view
getSelectedThreads() {
const selected = [];
// Find selected conversation items
const selectedElements = document.querySelectorAll(
'.conversation-item.selected, .thread-item.selected, ' +
'.conversation-item input[type="checkbox"]:checked, ' +
'[data-selected="true"]'
);
selectedElements.forEach(element => {
const conversationEl = element.closest('.conversation-item, .thread-item');
if (conversationEl) {
const threadInfo = {
id: conversationEl.getAttribute('data-conversation-id') ||
conversationEl.getAttribute('data-thread-id') ||
conversationEl.id,
subject: conversationEl.querySelector('.subject, .conversation-subject')?.textContent?.trim(),
sender: conversationEl.querySelector('.sender, .from')?.textContent?.trim()
};
if (threadInfo.id) {
selected.push(threadInfo);
}
}
});
return selected;
},
// Find the reply/compose box
findReplyBox() {
console.log('=== FINDING INSTANTLY REPLY BOX ===');
// Log all contenteditable elements for debugging
const allContentEditable = document.querySelectorAll('[contenteditable="true"]');
console.log(`Found ${allContentEditable.length} contenteditable elements:`, allContentEditable);
// Log all textareas
const allTextareas = document.querySelectorAll('textarea');
console.log(`Found ${allTextareas.length} textarea elements:`, allTextareas);
// Expanded list of selectors to try
const selectors = [
// Contenteditable variations
'div[contenteditable="true"]',
'[contenteditable="true"]',
'div[contenteditable="true"].reply-box',
'div[contenteditable="true"]:not([aria-label])', // Exclude Gmail-style elements
// Textarea variations
'textarea',
'textarea.reply-textarea',
'textarea.message-input',
'textarea[placeholder*="reply"]',
'textarea[placeholder*="message"]',
'textarea[placeholder*="write"]',
'textarea[placeholder*="type"]',
// Class-based selectors
'.reply-input',
'.message-input',
'.compose-input',
'.email-input',
'.message-composer',
'.compose-area',
'.reply-area',
'.message-box',
'.compose-box',
// Data attribute selectors
'[data-testid="message-input"]',
'[data-testid="reply-input"]',
'[data-testid="compose-input"]',
'[data-role="textbox"]',
'[role="textbox"]',
// Framework-specific selectors (React/Vue/Angular)
'[class*="reply"][class*="input"]',
'[class*="message"][class*="input"]',
'[class*="compose"][class*="input"]',
'[class*="editor"]',
'[class*="text-editor"]',
// Instantly-specific guesses
'.instantly-reply-box',
'.instantly-compose',
'#reply-box',
'#message-box',
// Check for nested structures
'.reply-container textarea',
'.reply-container [contenteditable="true"]',
'.message-container textarea',
'.message-container [contenteditable="true"]',
'.compose-container textarea',
'.compose-container [contenteditable="true"]'
];
console.log('Trying selectors:', selectors);
for (const selector of selectors) {
try {
const elements = document.querySelectorAll(selector);
console.log(`Selector "${selector}" found ${elements.length} elements`);
// Check each element to see if it's visible and likely a reply box
for (const element of elements) {
// Check if visible
if (element.offsetParent !== null) {
const rect = element.getBoundingClientRect();
const isVisible = rect.width > 0 && rect.height > 0;
const isReasonableSize = rect.width > 100 && rect.height > 50;
console.log(`Element matched by "${selector}":`, {
tagName: element.tagName,
className: element.className,
id: element.id,
placeholder: element.placeholder,
ariaLabel: element.getAttribute('aria-label'),
visible: isVisible,
size: `${rect.width}x${rect.height}`,
reasonableSize: isReasonableSize
});
if (isVisible && isReasonableSize) {
console.log(`Found potential reply box with selector: ${selector}`);
console.log('Element details:', element);
return element;
}
}
}
} catch (error) {
console.error(`Error with selector "${selector}":`, error);
}
}
console.error('=== COULD NOT FIND INSTANTLY REPLY BOX ===');
console.log('Please inspect the reply area and look for:');
console.log('1. The main input element (textarea or contenteditable div)');
console.log('2. Its class names, ID, or data attributes');
console.log('3. Any parent containers with identifiable classes');
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;
}
// Handle different input types
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') {
// For contenteditable divs
replyBox.innerHTML = draft.replace(/\n/g, '<br>');
replyBox.dispatchEvent(new Event('input', { bubbles: true }));
// Trigger any React/Vue change handlers
const inputEvent = new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: draft
});
replyBox.dispatchEvent(inputEvent);
}
console.log('Draft inserted into Instantly reply box');
return true;
}
};
// Make it globally available
window.InstantlyExtractor = InstantlyExtractor;