> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lettr.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Email Parsing

> Understand how Lettr parses inbound email into a structured relay payload with sender, recipients, subject, body, headers, and content.

When Lettr receives an email on your inbound domain, it automatically parses the raw email into a structured format. This page explains how different parts of the email are extracted and made available in the webhook payload.

## Parsed Email Structure

Inbound emails are delivered as `relay.relay_delivery` webhook events. Each event contains a relay message with parsed email content:

```json theme={null}
[
  {
    "msys": {
      "relay_message": {
        "msg_from": "sender@example.com",
        "rcpt_to": "support@mail.example.com",
        "friendly_from": "John Doe <sender@example.com>",
        "customer_id": "1234",
        "webhook_id": "wh_abc123",
        "content": {
          "html": "<html><body><p>HTML content...</p></body></html>",
          "text": "Plain text content of the email...",
          "subject": "Question about my order",
          "headers": [
            { "Message-ID": "<unique-id@example.com>" },
            { "Date": "Mon, 15 Jan 2024 10:30:00 +0000" },
            { "In-Reply-To": "<previous-id@example.com>" },
            { "References": "<original-id@example.com>" },
            { "Content-Type": "multipart/mixed" },
            { "To": ["support@mail.example.com"] },
            { "CC": ["team@mail.example.com"] }
          ],
          "to": ["support@mail.example.com"],
          "email_rfc822": "...raw RFC 822 MIME content..."
        }
      }
    }
  }
]
```

<Note>
  Webhook payloads are delivered as a JSON array of events. A single delivery may contain one or more events.
</Note>

## Sender Information

### From Address

The relay message provides two sender fields:

* `msg_from` — the envelope sender (SMTP MAIL FROM), always a bare email address
* `friendly_from` — the display name and address from the `From` header

```javascript theme={null}
const relay = event.msys.relay_message;

console.log(relay.msg_from);      // "sender@example.com"
console.log(relay.friendly_from); // "John Doe <sender@example.com>"
```

To extract just the email address or display name from `friendly_from`:

```javascript theme={null}
function parseFriendlyFrom(friendlyFrom) {
  const match = friendlyFrom.match(/^(.+?)\s*<([^>]+)>$/);
  if (match) {
    return { name: match[1].trim(), email: match[2] };
  }
  return { name: null, email: friendlyFrom };
}
```

### Reply-To Address

The Reply-To address is available in the `content.headers` array:

```javascript theme={null}
const replyToHeader = relay.content.headers.find(h => h['Reply-To']);
const replyAddress = replyToHeader?.['Reply-To'] || relay.msg_from;
```

## Recipients

### Recipients

The relay message provides:

* `rcpt_to` — the envelope recipient (the address on your inbound domain that received the email)
* `content.to` — the `To` header addresses from the email
* CC and BCC addresses are available in the `content.headers` array

```javascript theme={null}
const relay = event.msys.relay_message;

// Envelope recipient (your inbound address)
const recipient = relay.rcpt_to; // "support@mail.example.com"

// Header recipients
const toAddresses = relay.content.to; // ["support@mail.example.com"]
const ccHeader = relay.content.headers.find(h => h['CC']);
const ccAddresses = ccHeader?.['CC'] || [];
```

<Note>
  BCC recipients are typically not visible in received emails since they're stripped by the sending server.
</Note>

## Subject Line

The subject is available in the `content` object, decoded from any MIME encoding:

```javascript theme={null}
const { subject } = relay.content;

// Handles encoded subjects like:
// "=?UTF-8?B?SGVsbG8gV29ybGQ=?=" → "Hello World"
// "=?ISO-8859-1?Q?=C4pfel?=" → "Äpfel"
```

### Handling Missing Subjects

```javascript theme={null}
const subject = relay.content.subject || '(No Subject)';
```

## Email Body

### Plain Text and HTML

Emails can contain plain text, HTML, or both. These are available in the `content` object:

```javascript theme={null}
const { text, html } = relay.content;

// Use HTML if available, fall back to plain text
const displayContent = html || `<pre>${escapeHtml(text)}</pre>`;

// Or prefer plain text for processing
const processableContent = text || stripHtml(html);
```

### Extracting Text from HTML

```javascript theme={null}
import { convert } from 'html-to-text';

function getPlainText(email) {
  if (email.text) {
    return email.text;
  }

  if (email.html) {
    return convert(email.html, {
      wordwrap: 130,
      selectors: [
        { selector: 'a', options: { ignoreHref: true } },
        { selector: 'img', format: 'skip' }
      ]
    });
  }

  return '';
}
```

### Handling Quoted Content

Strip quoted replies to get just the new content:

```javascript theme={null}
function stripQuotedContent(text) {
  const lines = text.split('\n');
  const newContent = [];

  for (const line of lines) {
    // Stop at common quote markers
    if (line.match(/^>/) ||
        line.match(/^On .+ wrote:/) ||
        line.match(/^-{3,}.*Original Message.*-{3,}/i) ||
        line.match(/^From:.*@/)) {
      break;
    }
    newContent.push(line);
  }

  return newContent.join('\n').trim();
}

const newReplyContent = stripQuotedContent(relay.content.text);
```

## Email Headers

### Standard Headers

Headers are available in `content.headers` as an array of single-key objects:

| Header         | Description                              |
| -------------- | ---------------------------------------- |
| `Message-ID`   | Unique identifier for this email         |
| `Date`         | When the email was sent                  |
| `In-Reply-To`  | Message-ID of the email being replied to |
| `References`   | Chain of Message-IDs in the conversation |
| `Content-Type` | MIME type of the email body              |
| `X-Mailer`     | Email client used to send                |
| `X-Priority`   | Email priority (1=High, 3=Normal, 5=Low) |

```javascript theme={null}
const { headers } = relay.content;

// Helper to find a header value
function getHeader(headers, name) {
  const header = headers.find(h => h[name] !== undefined);
  return header ? header[name] : null;
}

// Get the message thread
const messageId = getHeader(headers, 'Message-ID');
const inReplyTo = getHeader(headers, 'In-Reply-To');
const references = getHeader(headers, 'References')?.split(/\s+/) || [];

// Check priority
const priority = getHeader(headers, 'X-Priority');
const isHighPriority = priority === '1' || priority === '2';
```

### Custom Headers

Access any header using the helper:

```javascript theme={null}
// Custom headers set by the sender
const customHeader = getHeader(headers, 'X-Custom-Header');
const mailingList = getHeader(headers, 'List-Id');
const unsubscribeUrl = getHeader(headers, 'List-Unsubscribe');
```

## Date and Time

### Sent Timestamp

The original send time is in the `Date` header:

```javascript theme={null}
const dateHeader = getHeader(relay.content.headers, 'Date');
const sentAt = new Date(dateHeader);
```

## Character Encoding

Lettr automatically handles character encoding conversion:

```javascript theme={null}
// These are all decoded to UTF-8:
// - ISO-8859-1 encoded content
// - Windows-1252 encoded content
// - Base64 encoded UTF-8
// - Quoted-Printable encoded content

const { text, subject } = relay.content;
// Both are UTF-8 strings regardless of original encoding
```

## Multipart Emails

Emails with multiple parts (text + HTML + attachments) are automatically parsed:

```javascript theme={null}
// A multipart email:
// - text/plain part → relay.content.text
// - text/html part → relay.content.html
// - Full raw MIME → relay.content.email_rfc822

const hasRichContent = relay.content.html && relay.content.text;
```

The `email_rfc822` field contains the complete raw MIME message, which you can parse with a MIME library if you need access to attachments or other parts not extracted into the structured fields.

## Raw MIME Parsing

For advanced use cases like extracting inline images, embedded attachments, or handling complex MIME structures, parse the raw `email_rfc822` content with a MIME library:

```javascript theme={null}
import { simpleParser } from 'mailparser';

async function parseRawEmail(relay) {
  const parsed = await simpleParser(relay.content.email_rfc822);

  return {
    from: parsed.from?.value,
    to: parsed.to?.value,
    subject: parsed.subject,
    text: parsed.text,
    html: parsed.html,
    attachments: parsed.attachments // includes inline images with cid
  };
}
```

## Parsing Examples

### Extract Email Signature

```javascript theme={null}
function extractSignature(text) {
  // Common signature delimiters
  const delimiters = [
    /^--\s*$/m,           // Standard delimiter
    /^_{3,}$/m,           // Underscores
    /^-{3,}$/m,           // Dashes
    /^Sent from my /m,    // Mobile signatures
    /^Get Outlook for /m  // Outlook mobile
  ];

  let signatureStart = text.length;

  for (const delimiter of delimiters) {
    const match = text.match(delimiter);
    if (match && match.index < signatureStart) {
      signatureStart = match.index;
    }
  }

  return {
    body: text.substring(0, signatureStart).trim(),
    signature: text.substring(signatureStart).trim()
  };
}
```

### Detect Email Client

```javascript theme={null}
function detectEmailClient(headers) {
  const mailer = getHeader(headers, 'X-Mailer') || getHeader(headers, 'User-Agent') || '';
  const received = getHeader(headers, 'Received') || '';

  if (mailer.includes('Microsoft Outlook')) return 'Outlook';
  if (mailer.includes('Apple Mail')) return 'Apple Mail';
  if (mailer.includes('Thunderbird')) return 'Thunderbird';
  if (received.includes('gmail.com')) return 'Gmail';
  if (received.includes('outlook.com')) return 'Outlook.com';

  return 'Unknown';
}
```

### Parse Structured Data

```javascript theme={null}
// Extract order number from confirmation emails
function extractOrderNumber(email) {
  const content = email.text || email.html;

  const patterns = [
    /order\s*#?\s*:?\s*([A-Z0-9-]+)/i,
    /order\s+number\s*:?\s*([A-Z0-9-]+)/i,
    /confirmation\s*#?\s*:?\s*([A-Z0-9-]+)/i
  ];

  for (const pattern of patterns) {
    const match = content.match(pattern);
    if (match) return match[1];
  }

  return null;
}
```

## Handling Edge Cases

### Empty Content

```javascript theme={null}
function getEmailContent(email) {
  const text = email.text?.trim();
  const html = email.html?.trim();

  if (!text && !html) {
    return {
      hasContent: false,
      content: '',
      contentType: 'none'
    };
  }

  return {
    hasContent: true,
    content: text || stripHtml(html),
    contentType: text ? 'text' : 'html'
  };
}
```

### Malformed Addresses

```javascript theme={null}
function parseEmailAddress(address) {
  if (!address) return null;

  // Handle various formats
  const match = address.match(/<([^>]+)>/) || // "Name <email>"
                address.match(/([^\s<>]+@[^\s<>]+)/); // bare email

  return match ? match[1].toLowerCase() : null;
}
```

## Related Topics

<CardGroup cols={2}>
  <Card title="Webhooks" icon="webhook" href="/learn/webhooks/event-types">
    Full webhook payload reference
  </Card>

  <Card title="Attachments" icon="paperclip" href="/learn/inbound/attachments">
    Handle file attachments
  </Card>

  <Card title="Reply Tracking" icon="reply" href="/learn/inbound/reply-tracking">
    Track conversation threads
  </Card>

  <Card title="Routing" icon="route" href="/learn/inbound/routing">
    Route emails based on content
  </Card>
</CardGroup>
