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:
From: Alice <alice@example.com>
To: Bob <bob@example.com>
Subject: Q3 numbers
Date: Tue, 12 Aug 2026 09:14:00 +0000
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="_boundary_42"
--_boundary_42
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
<p>Here are the numbers you asked for.</p>
--_boundary_42
Content-Type: application/pdf; name="report.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="report.pdf"
JVBERi0xLjQKJc... (base64 continues)
--_boundary_42--
Three things to notice:
- Headers come first, one per line, until the first blank line.
-
Content-Type: multipart/...; boundary="..."means the body is split into parts, separated by--<boundary>. - Each part has its own headers and its own encoding — usually
quoted-printablefor text andbase64for 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 };
}
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 }));
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.emlwrapped 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)