'use strict'; /** * bodyParser.js * Heuristic cleaner for email body text returned by Microsoft Graph API. * Strips boilerplate noise without touching actual message content. * All rules are based on observed real-world patterns from this mailbox. */ // --------------------------------------------------------------------------- // Pattern library — ordered by application sequence // --------------------------------------------------------------------------- // 1. HTML entities produced by Graph API's text/plain conversion const HTML_ENTITIES = [ [/ /gi, ' '], [/&/gi, '&'], [/>/gi, '>'], [/</gi, '<'], [/"/gi, '"'], [/'/gi, "'"], ]; // 2. External email caution banners — appear at start of body or inline // Covers variations with/without "This email originated..." sentence const CAUTION_BANNERS = [ // Full two-sentence form with bold/marker text /CAUTION[\s\-–]*EXTERNAL\s+EMAIL\s*:.*?(?:content is safe\.?)/gis, // Short form /CAUTION\s*:?\s*This email originated from outside.*?(?:content is safe\.?)/gis, ]; // 3. Legal boilerplate blocks — DISCLAIMER and CONFIDENTIALITY NOTICE // These repeat on every reply in a chain. Match greedy to end of block. const LEGAL_BLOCKS = [ // DISCLAIMER block (YMCA, others) /DISCLAIMER\s*:.*?(?=DISCLAIMER\s*:|CONFIDENTIALITY\s*NOTICE\s*:|$)/gis, // CONFIDENTIALITY NOTICE block (Prime HHCC, Farber & Lindley, others) /CONFIDENTIALITY\s*NOTICE\s*:.*?(?=DISCLAIMER\s*:|CONFIDENTIALITY\s*NOTICE\s*:|$)/gis, ]; // 4. Signature block delimiters — everything from a recognized sig opener onward // Only applied when stripping signatures is explicitly requested (see exports). const SIGNATURE_DELIMITERS = [ // Standard triple-dash separator /^---\s*$/m, // deRenzy signature pattern: name on one line, then IT@/Seton@ email lines /^Seton Carmichael\s*\n(?:IT@|Seton@)/m, /^Richard Priest\s*\n/m, ]; // --------------------------------------------------------------------------- // HTML → plain text conversion // --------------------------------------------------------------------------- /** * Convert HTML email body to plain text, preserving line structure. * Two-pass: structural tags → newlines first, then strip remaining tags. * @param {string} html * @returns {string} */ function htmlToText(html) { let text = html; // Mark Outlook signature boundaries before conversion. // Outlook wraps signatures in id="Signature" (or id="x_Signature" on nested messages). // We replace the opening tag with a sentinel [SIG] so callers can decide whether to // keep or strip the signature after HTML→text conversion. text = text.replace(/]*\bid=["'][^"']*[Ss]ignature["'][^>]*>/gi, '\n[SIG]\n'); // Block-level tags that should become newlines text = text.replace(//gi, '\n'); text = text.replace(/<\/(?:div|p|tr|li|blockquote|h[1-6])>/gi, '\n'); // Strip all remaining tags text = text.replace(/<[^>]+>/g, ''); return text; } // --------------------------------------------------------------------------- // Core cleaner // --------------------------------------------------------------------------- /** * Clean a single email body string. Handles both HTML and plain text input. * @param {string} body - Raw body text or HTML from Graph API * @param {object} [opts] * @param {boolean} [opts.stripSignature=false] - Also strip trailing signature block * @returns {string} Cleaned plain text */ function cleanBody(body, opts = {}) { if (!body || typeof body !== 'string') return body || ''; let text = body; // Step 1: Convert HTML to plain text if needed if (/]/i.test(text) || /]/i.test(text) || /]/i.test(text)) { text = htmlToText(text); } // Step 2: Normalize HTML entities (may remain after tag stripping) for (const [pattern, replacement] of HTML_ENTITIES) { text = text.replace(pattern, replacement); } // Step 2: Strip external caution banners for (const pattern of CAUTION_BANNERS) { text = text.replace(pattern, ''); } // Step 3: Strip legal boilerplate blocks for (const pattern of LEGAL_BLOCKS) { text = text.replace(pattern, ''); } // Step 4: Handle [SIG] sentinel (injected by htmlToText for id="Signature" divs) if (opts.stripSignature) { // Remove from [SIG] marker onward text = text.replace(/\n?\[SIG\][\s\S]*/g, ''); } else { // Keep signature content, just remove the marker itself text = text.replace(/\[SIG\]\n?/g, ''); } // Step 4b: Text-based signature delimiters (fallback for non-HTML emails) if (opts.stripSignature) { for (const delimiter of SIGNATURE_DELIMITERS) { const match = text.search(delimiter); if (match !== -1) { text = text.slice(0, match); break; } } } // Step 5: Collapse runs of 3+ blank lines down to 2, trim edges text = text.replace(/\n{3,}/g, '\n\n'); text = text.trim(); return text; } /** * Clean the body field of a Graph API email message object in-place. * Returns the same object with body.content cleaned. * Also cleans bodyPreview if present. * @param {object} message - Graph API message object * @param {object} [opts] - Same options as cleanBody * @returns {object} The same message object, mutated */ function cleanMessage(message, opts = {}) { if (!message) return message; if (message.body && message.body.content) { message.body.content = cleanBody(message.body.content, opts); } // bodyPreview is a short excerpt — just do entity decode and trim if (message.bodyPreview) { message.bodyPreview = cleanBody(message.bodyPreview, { stripSignature: false }); } return message; } /** * Clean an array of message objects. * @param {object[]} messages * @param {object} [opts] * @returns {object[]} */ function cleanMessages(messages, opts = {}) { if (!Array.isArray(messages)) return messages; return messages.map(m => cleanMessage(m, opts)); } module.exports = { cleanBody, cleanMessage, cleanMessages };