// 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 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, '
'); 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;