DEV Community

Casey Marlin
Casey Marlin

Posted on

What's actually inside an .eml file (and how to read one without Outlook)

Someone forwards you an email as a .eml attachment. You double-click it,
and… nothing useful happens. On Windows it tries to launch Outlook; on a
work laptop with no mail client you get a wall of raw text. So what is
this file, and how do you read it programmatically?

An .eml file is just text

Despite the scary-looking contents, a .eml file is a plain-text file in
MIME format (RFC 5322 + RFC 2045). Open one in a text editor and the
top looks like this:

Enter fullscreen mode Exit fullscreen mode

Three things to notice:

  1. Headers come first, one per line, until the first blank line.
  2. Content-Type: multipart/...; boundary="..." means the body is split into parts, separated by --<boundary>.
  3. Each part has its own headers and its own encoding — usually quoted-printable for text and base64 for attachments.

Reading it in JavaScript

The nice thing about MIME being plain text is that you can parse it in the
browser with the File API — no server, no upload. The skeleton:

async function readEml(file) {
  const raw = await file.text();

  // 1. Split headers from body at the first blank line
  const sep = raw.indexOf("\r\n\r\n") !== -1 ? "\r\n\r\n" : "\n\n";
  const [rawHeaders, body] = splitOnce(raw, sep);

  // 2. Parse headers into a map (careful: values can wrap onto
  //    continuation lines that start with whitespace)
  const headers = parseHeaders(rawHeaders);

  // 3. If multipart, split the body on the boundary and recurse
  const ctype = headers["content-type"] || "";
  const boundary = /boundary="?([^";]+)"?/i.exec(ctype)?.[1];
  const parts = boundary
    ? body.split(`--${boundary}`).slice(1, -1).map(parseMimePart)
    : [{ headers, body }];

  return { headers, parts };
}
Enter fullscreen mode Exit fullscreen mode

The two places people get bitten:

  • Header folding. A long Subject: can wrap onto the next line if that line starts with a space or tab. Un-fold before you parse, or you'll truncate subjects.
  • Encoded words. Non-ASCII headers look like =?UTF-8?B?SsOpc3Vz?= (RFC 2047). Decode them or "José" turns into line noise.

Attachments are just the base64 part decoded into a Blob, which you can
hand straight to a download link:

const bytes = Uint8Array.from(atob(part.body.replace(/\s/g, "")),
                              c => c.charCodeAt(0));
const url = URL.createObjectURL(new Blob([bytes], { type: mimeType }));
Enter fullscreen mode Exit fullscreen mode

The gotchas that make it a real project

A toy parser handles the happy path. A robust one has to deal with:

  • .emlx (Apple Mail) — an .eml wrapped with a length prefix and a plist footer you have to strip.
  • .msg (Outlook) — not MIME at all; it's an OLE compound binary file that needs a completely different parser.
  • Inline images referenced by cid: in the HTML body, which live as separate MIME parts.
  • Rendering untrusted HTML safely (sandboxed iframe, scripts stripped) so a phishing sample can't run anything.

I got tired of re-solving this, so I built a free browser-based viewer that
handles all of the above —
emltool.com — everything is parsed locally with the
File API, nothing is uploaded. Source of truth if you just need to open
an .eml/.emlx/.msg file without installing anything.

Hope the format breakdown saves someone an afternoon of squinting at
base64. If you're implementing your own parser, the RFC 2047 encoded-word
decoding is the part worth testing hardest.

Top comments (0)