478 lines
No EOL
18 KiB
JavaScript
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;
|