2026-03-26 15:00:00+00:00

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:

  1. The Outlook Attachment Dropdown Bug: Strict email clients hide inline images (packaged under standard multipart/related MIME envelopes) from the master attachment bar/dropdown, making files hard to download.
  2. 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 into tags, breaking the image source entirely.
  3. 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:

LEGAY ENVELOPE (Hides Attachments) RESILIENT ENVELOPE (Shows Attachments) Root: multipart/related Child: multipart/alternative (Body) Nested Part: Inline Attachments (Cid) Root: multipart/mixed Sibling 1 multipart/alternative Nests: text/plain & text/html Sibling 2 & 3 Direct Attachments Content-Disposition: attachment

🛡️ 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:

  1. Flawless Outlook Compatibility: Migrating to a root multipart/mixed container ensures that all images and files are displayed clearly in Outlook’s master attachment bar, while remaining resolvable inline.
  2. 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.
  3. Resilient Filename Resolution: Upgrading search matchers to support unencoded, encodeURI, and encodeURIComponent filenames ensures patient uploads containing spaces never generate broken image frames.
  4. 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.