Modern clinical portal users (patients and clinicians alike) expect messaging interfaces to look and feel like standard chat apps. They type in markdown format, copy-paste screenshots, and upload multiple document attachments. However, converting these rich chat messages into HTML notifications delivered to standard email clients (like Outlook or Gmail) introduces complex engineering challenges.
Standard web browsers render markdown seamlessly. But when that markdown is converted to HTML and emailed, developers regularly hit major rendering bugs:
- The Outlook Attachment Dropdown Bug: Strict email clients hide inline images (packaged under standard
multipart/relatedMIME envelopes) from the master attachment bar/dropdown, making files hard to download. - Markdown-MIME Parser Conflicts: Standard regex-based markdown-to-HTML italic parsers convert multiple underscores inside an HTML
src="cid:cid_image_name_1_png"attribute intotags, breaking the image source entirely. - URL-Encoded Spacing Fails: Filenames containing spaces are encoded in markdown as
%20(e.g.!alt). Traditional regex matchers searching for the literal metadata title fail to resolve the reference, leaving image links broken.
This article details how to design and build a Safe Rendering Markdown-to-Email Pipeline in Node.js, resolving these parser conflicts, structuring MIME trees securely for Outlook compliance, and executing dispatches via AWS SES.
🏗️ Overhauling the MIME Structure for Outlook Compliance
The choice of root MIME container dictates how email clients display attachments. Originally, many notification engines package raw emails under a root multipart/related container. While this works for displaying images inline, strict email clients like Outlook treat these parts as presentation-only resources and completely hide them from the attachment dropdown.
To force email clients to display the attachment dropdown/bar under the "To:" header while still resolving the Content-ID (cid:) inline images correctly, we must restructure our MIME tree to a root multipart/mixed container. In this structure, the HTML body (packaged inside multipart/alternative) and the attachments reside as sibling nodes:
🛡️ Resolving Markdown-to-MIME Parser Conflicts
Converting dynamic markdown input directly to email structures introduces two subtle, silent bugs that break rendering completely.
1. The Underscore-Emphasis Corruption
When generating inline images via Content-IDs (cid:), we originally construct identifiers using a slugified format containing underscores: src="cid:cid_Screenshot_from_2026_05_26_15_00_36_png"
During the HTML compile phase, standard markdown-to-HTML italic parsers find the multiple underscores inside the src attribute values and convert them into HTML emphasis () tags, corrupting the image source: src="cid:cidScreenshotfrom20260526150036_png"
The Fix: Sanitize the Content-ID to be strictly alphanumeric. By stripping all underscores and special characters ('cid' + title.replace(/[^a-zA-Z0-9]/g, '')), the identifier is rendered completely immune to markdown italic/emphasis parser triggers.
2. Space-to-%20 URL Encoding Resolution
Patients frequently upload files containing raw spaces (e.g. Screenshot 2026.png). Markdown parsers automatically convert spaces in URL links to %20 values: !Image
However, the file attachment metadata contains raw spaces. If our replacement regex searches strictly for the literal title containing spaces, it fails to find the URL-encoded match, leaving the original markdown reference completely broken.
The Fix: Upgrade our search matcher to dynamically check the literal title, the standard URI-encoded encodeURI(title), and encodeURIComponent(title) parameters to guarantee replacement mapping regardless of space representations.
🛠️ The Complete Email Rendering and MIME Ingestion Engine
Below is the production-ready Node.js/TypeScript email generation engine. It processes markdown content, sanitizes Content-IDs to prevent formatting leaks, resolves URL-encoded attachment links, builds the multipart/mixed raw MIME structure, and delivers it via the AWS SES SDK SendRawEmailCommand:
// email-renderer.ts (Resilient Markdown-to-MIME Notification Engine)
import { SESClient, SendRawEmailCommand } from '@aws-sdk/client-ses';
import { logger } from './logger.js';
const ses = new SESClient({ region: 'us-east-1' });
interface AttachmentMetadata {
title: string;
contentType: string;
buffer: Buffer;
}
export class ResilientEmailRenderer {
/**
* Main compilation pipeline: Markdown HTML rendering -> Content-ID Mapping -> SES Dispatch
*/
public async sendCompiledEmail(
to: string,
subject: string,
markdownBody: string,
attachments: AttachmentMetadata[]
): Promise<void> {
// 1. First Pass: Build Sanitized Content-IDs for inline replacements
const inlineCids = attachments.map(att => ({
title: att.title,
// Strictly Alphanumeric Content-ID: Immune to Markdown emphasis rules!
cid: 'cid' + att.title.replace(/[^a-zA-Z0-9]/g, '')
}));
// 2. Second Pass: Convert Markdown to HTML and replace attachment links
let htmlContent = this.compileMarkdownToHtml(markdownBody);
htmlContent = this.resolveAttachmentUrls(htmlContent, inlineCids);
const plainTextContent = this.stripHtml(htmlContent);
// 3. Third Pass: Build the strict RFC 2822 compliant multipart/mixed MIME message
const boundary = '----=_Part_' + Math.random().toString(36).substring(2);
const mimeParts: string[] = [];
// Master MIME Headers
mimeParts.push(`To: ${to}`);
mimeParts.push(`Subject: ${subject}`);
mimeParts.push('MIME-Version: 1.0');
mimeParts.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
mimeParts.push(''); // Header-Body separation
// Sibling Part 1: multipart/alternative (Plain Text & HTML bodies)
const altBoundary = boundary + '_Alt';
mimeParts.push(`--${boundary}`);
mimeParts.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
mimeParts.push('');
// Plain Text Fallback
mimeParts.push(`--${altBoundary}`);
mimeParts.push('Content-Type: text/plain; charset=UTF-8');
mimeParts.push('Content-Transfer-Encoding: 7bit');
mimeParts.push('');
mimeParts.push(plainTextContent);
mimeParts.push('');
// Primary HTML Body
mimeParts.push(`--${altBoundary}`);
mimeParts.push('Content-Type: text/html; charset=UTF-8');
mimeParts.push('Content-Transfer-Encoding: 7bit');
mimeParts.push('');
mimeParts.push(htmlContent);
mimeParts.push('');
mimeParts.push(`--${altBoundary}--`);
// Sibling Part 2: Attachments (Marked visible via "Content-Disposition: attachment")
for (let i = 0; i < attachments.length; i++) {
const att = attachments[i];
const cidMapping = inlineCids[i];
mimeParts.push(`--${boundary}`);
mimeParts.push(`Content-Type: ${att.contentType}; name="${att.title}"`);
mimeParts.push(`Content-Transfer-Encoding: base64`);
mimeParts.push(`Content-ID: <${cidMapping.cid}>`);
// Disposition "attachment" forces Outlook to show the master file bar
mimeParts.push(`Content-Disposition: attachment; filename="${att.title}"`);
mimeParts.push('');
mimeParts.push(att.buffer.toString('base64').match(/.{1,76}/g)?.join('\r\n') || '');
mimeParts.push('');
}
mimeParts.push(`--${boundary}--`);
// Normalize all line breaks strictly to CRLF (\r\n) per RFC 2822
const rawMimeMessage = mimeParts.join('\r\n');
// 4. Send via AWS SES SendRawEmailCommand
try {
await ses.send(new SendRawEmailCommand({
RawMessage: {
Data: Buffer.from(rawMimeMessage)
}
}));
logger.info({ to, subject }, 'Successfully delivered compliant raw MIME message.');
} catch (err: any) {
logger.error({ err }, 'Failed to dispatch raw MIME email.');
throw err;
}
}
/**
* Resolves unencoded and URL-encoded spacing patterns in markdown image/link links
*/
private resolveAttachmentUrls(html: string, mappings: { title: string; cid: string }[]): string {
let resolved = html;
for (const map of mappings) {
const title = map.title;
const encodedTitle = encodeURI(title);
const componentEncodedTitle = encodeURIComponent(title);
// Match raw title, spaces as %20, and full URI encoded filename variants
const pattern = new RegExp(
`src=["'](${this.escapeRegex(title)}|${this.escapeRegex(encodedTitle)}|${this.escapeRegex(componentEncodedTitle)})["']`,
'gi'
);
resolved = resolved.replace(pattern, `src="cid:${map.cid}"`);
}
return resolved;
}
private compileMarkdownToHtml(markdown: string): string {
let html = markdown;
// Bold compilation
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// Italic compilation (Underscore emphasis matches)
html = html.replace(/_(.*?)_/g, '<em>$1</em>');
// Image matching
html = html.replace(/!\[(.*?)\]\((.*?)\)/g, '<img alt="$1" src="$2" />');
return html;
}
private stripHtml(html: string): string {
return html.replace(/<[^>]*>/g, '');
}
private escapeRegex(string: string): string {
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
}
📈 Summary of Benefits
Adapting your email notification engine to handle complex markdown-to-MIME conversions establishes a rock-solid, HIPAA-compliant patient communication pipeline:
- Flawless Outlook Compatibility: Migrating to a root
multipart/mixedcontainer ensures that all images and files are displayed clearly in Outlook’s master attachment bar, while remaining resolvable inline. - Eliminates Formatting Leaks: Sanitizing Content-IDs to strictly alphanumeric strings blocks regex-based markdown parsers from converting underscores into corrupted
attributes inside HTML code blocks. - Resilient Filename Resolution: Upgrading search matchers to support unencoded,
encodeURI, andencodeURIComponentfilenames ensures patient uploads containing spaces never generate broken image frames. - RFC 2822 Compliance: Normalizing raw MIME message headers and boundary separations to strict CRLF (
\r\n) prevents duplicate Content-Type bugs and ensures consistent delivery rates.
By engineering clinical communication platforms around strict MIME boundaries and sanitizing user-generated markdown inputs proactively, you build a state-of-the-art notifications pipeline that guarantees patient data is delivered safely and displayed beautifully across all email software clients.