outlook-mcp/utils/bodyParser.js
Seton Carmichael a7886b5b2b Initial commit: Outlook MCP Server v1.0.0
MCP server providing Microsoft Outlook integration via Graph API:
- Email: list, search, read, send, thread reconstruction
- Calendar: list, create, decline, cancel, delete events
- Folders: list, create, move emails
- Inbox rules: list, create, reorder
- MSAL device code flow auth with persistent token cache
- Test mode with mock data
- Comprehensive README with setup, config, and tool reference

20 MCP tools across 5 modules. Node.js >= 14. MIT license.
2026-06-21 19:39:31 -04:00

173 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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, '>'],
[/&lt;/gi, '<'],
[/&quot;/gi, '"'],
[/&#39;/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(/<div[^>]*\bid=["'][^"']*[Ss]ignature["'][^>]*>/gi, '\n[SIG]\n');
// Block-level tags that should become newlines
text = text.replace(/<br\s*\/?>/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 (/<html[\s>]/i.test(text) || /<body[\s>]/i.test(text) || /<div[\s>]/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 };