Opens received email in the page, with nothing sent anywhere: an .eml, an
Outlook .msg, a winmail.dat, an .mbox mailbox of thousands of messages, or
a folder's worth of messages at once.
9 properties · 5 events · 14 methods · 5 parts
Example
Try it with your own files in confidence: they are opened by your browser, on your device. Nothing is uploaded, no copy reaches us or anyone else, and nothing is kept once you close this tab.
A fictional design studio's mailbox: a project conversation, a newsletter whose tracking picture is blocked until you ask (its banner travels with the message, so it shows), a meeting request, files to open and save - a picture, a spreadsheet and a forwarded message - and a message in Japanese. Drop your own .eml, .msg or .mbox files on it, or use Open.
Show markup
<div class="row" style="gap: 0.75rem; flex-wrap: wrap; align-items: center; margin-block-end: 0.75rem">
<button type="button" id="ex-mv-box">Open the sample mailbox</button>
<button type="button" id="ex-mv-one">Open one message</button>
<label><input type="checkbox" id="ex-mv-threads" /> Conversations</label>
</div>
<bmx-mail-viewer id="ex-mv" locale="en-GB" label="Sample mailbox" style="--bmx-mail-viewer-height: 46rem"></bmx-mail-viewer>
<p class="note" id="ex-mv-out" role="status">A fictional design studio's mailbox: a project conversation, a newsletter whose tracking picture is blocked until you ask (its banner travels with the message, so it shows), a meeting request, files to open and save - a picture, a spreadsheet and a forwarded message - and a message in Japanese. Drop your own .eml, .msg or .mbox files on it, or use Open.</p>
<script type="module">
await customElements.whenDefined('bmx-mail-viewer');
const el = document.getElementById('ex-mv');
const out = document.getElementById('ex-mv-out');
const b64 = text => btoa(String.fromCharCode(...new TextEncoder().encode(text))).replace(/.{76}/g, '$&\r\n');
const word = text => `=?UTF-8?B?${btoa(String.fromCharCode(...new TextEncoder().encode(text)))}?=`;
// A banner and a photo, drawn here so the sample needs no files.
const picture = (w, h, draw) => {
const c = document.createElement('canvas');
c.width = w;
c.height = h;
draw(c.getContext('2d'), w, h);
return c.toDataURL('image/png').split(',')[1].replace(/.{76}/g, '$&\r\n');
};
const banner = picture(600, 160, (g, w, h) => {
const grad = g.createLinearGradient(0, 0, w, h);
grad.addColorStop(0, '#0f766e');
grad.addColorStop(1, '#1e3a8a');
g.fillStyle = grad;
g.fillRect(0, 0, w, h);
g.fillStyle = '#fff';
g.font = '600 34px system-ui, sans-serif';
g.fillText('Lumen & Loom', 32, 74);
g.font = '18px system-ui, sans-serif';
g.fillText('Autumn studio notes', 34, 110);
});
const photo = picture(480, 320, (g, w, h) => {
g.fillStyle = '#f5efe6';
g.fillRect(0, 0, w, h);
g.fillStyle = '#c2410c';
g.fillRect(60, 120, 200, 140);
g.fillStyle = '#334155';
g.fillRect(290, 80, 120, 180);
g.fillStyle = '#0f766e';
g.beginPath();
g.arc(170, 90, 40, 0, Math.PI * 2);
g.fill();
});
const day = n => new Date(Date.UTC(2026, 9, 12 + n, 9 + (n % 5), 12 + n * 3)).toUTCString().replace('GMT', '+0000');
const messages = [
`From: Maya Lindqvist <maya@lumenloom.example>
To: Studio <studio@lumenloom.example>
Subject: Harbour Street café - first sketches
Date: ${day(0)}
Message-ID: <hs1@lumenloom.example>
Content-Type: multipart/mixed; boundary="b1"
--b1
Content-Type: text/html; charset=utf-8
<p>Hi all,</p><p>Here is the first pass for the <b>Harbour Street café</b>: warm timber, terracotta and a green that picks up the sea outside.</p><p>The <a href="https://lumenloom.example/projects/harbour">project page</a> has the brief. Thoughts by Thursday?</p><p>Maya</p>
--b1
Content-Type: image/png; name="harbour-sketch.png"
Content-Disposition: attachment; filename="harbour-sketch.png"
Content-Transfer-Encoding: base64
${photo}
--b1
Content-Type: text/csv; name="materials.csv"
Content-Disposition: attachment; filename="materials.csv"
Content-Transfer-Encoding: base64
${b64('Item,Finish,Supplier,Cost\nCounter top,Oiled oak,Northwood,1240\nWall tiles,Terracotta,Clay & Co,860\nPendants,Brushed brass,Lumen Works,420\n')}
--b1--
`,
`From: Tom Achebe <tom@lumenloom.example>
To: Maya Lindqvist <maya@lumenloom.example>
Cc: Studio <studio@lumenloom.example>
Subject: Re: Harbour Street café - first sketches
Date: ${day(1)}
Message-ID: <hs2@lumenloom.example>
In-Reply-To: <hs1@lumenloom.example>
References: <hs1@lumenloom.example>
Content-Type: text/plain; charset=utf-8; format=flowed
Love the terracotta. Could we try the pendants a touch lower over the ${''}
counter? I have attached nothing - just thoughts.
> Here is the first pass for the Harbour Street café: warm timber,
> terracotta and a green that picks up the sea outside.
Tom
`,
`From: Priya Nair <priya@lumenloom.example>
To: Maya Lindqvist <maya@lumenloom.example>
Subject: Re: Harbour Street café - first sketches
Date: ${day(2)}
Message-ID: <hs3@lumenloom.example>
In-Reply-To: <hs2@lumenloom.example>
References: <hs1@lumenloom.example> <hs2@lumenloom.example>
X-Priority: 1
Content-Type: text/plain; charset=utf-8
The client wants to see it on Monday - can we have boards ready by Friday?
Priya
`,
`From: Lumen & Loom Notes <notes@lumenloom.example>
To: studio@lumenloom.example
Subject: ${word('Autumn studio notes 🍂')}
Date: ${day(3)}
Message-ID: <news9@lumenloom.example>
Content-Type: multipart/related; boundary="r1"
--r1
Content-Type: text/html; charset=utf-8
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f4f1ea"><tr><td align="center" style="padding:24px"><table width="600" style="background:#ffffff;border-radius:8px;overflow:hidden"><tr><td><img src="cid:banner@notes" width="600" alt="Lumen & Loom autumn notes"></td></tr><tr><td style="padding:24px;font-family:Georgia,serif;font-size:16px;line-height:1.6;color:#1f2937"><h1 style="font-size:24px;margin:0 0 12px">Three colours for the season</h1><p>Rust, moss and a soft stone: what we are reaching for this autumn, and why.</p><p><a href="https://lumenloom.example/notes/autumn" style="display:inline-block;background:#0f766e;color:#fff;padding:10px 18px;border-radius:6px;text-decoration:none">Read the notes</a></p></td></tr></table><img src="https://tracking.example.net/open.gif?id=8841" width="1" height="1" alt=""></td></tr></table>
--r1
Content-Type: image/png
Content-ID: <banner@notes>
Content-Transfer-Encoding: base64
${banner}
--r1--
`,
`From: Priya Nair <priya@lumenloom.example>
To: Studio <studio@lumenloom.example>
Subject: Client review: Harbour Street
Date: ${day(4)}
Message-ID: <inv1@lumenloom.example>
Content-Type: multipart/alternative; boundary="a1"
--a1
Content-Type: text/plain; charset=utf-8
Review of the Harbour Street boards with the client.
--a1
Content-Type: text/calendar; method=REQUEST; charset=utf-8
BEGIN:VCALENDAR
METHOD:REQUEST
BEGIN:VEVENT
UID:inv1@lumenloom.example
SUMMARY:Client review: Harbour Street
DTSTART:20261019T130000Z
DTEND:20261019T140000Z
LOCATION:Studio meeting room
ORGANIZER;CN=Priya Nair:mailto:priya@lumenloom.example
END:VEVENT
END:VCALENDAR
--a1--
`,
`From: Sam Rivera <sam@lumenloom.example>
To: Maya Lindqvist <maya@lumenloom.example>
Subject: Fwd: Lighting quote
Date: ${day(5)}
Message-ID: <fw1@lumenloom.example>
Content-Type: multipart/mixed; boundary="m2"
--m2
Content-Type: text/plain; charset=utf-8
Forwarding the quote from Lumen Works - see the attached message.
--m2
Content-Type: message/rfc822
From: Lumen Works <sales@lumenworks.example>
To: Sam Rivera <sam@lumenloom.example>
Subject: Quote 2026-118: brushed brass pendants
Date: ${day(4)}
Content-Type: text/plain; charset=utf-8
Six pendants in brushed brass, 1.2 m drop: 420.00 each, delivery in three weeks.
--m2--
`,
`From: ${word('佐藤 花子')} <hanako@kyoto-textiles.example>
To: Maya Lindqvist <maya@lumenloom.example>
Subject: ${word('生地サンプルの件')}
Date: ${day(6)}
Message-ID: <jp1@kyoto-textiles.example>
Content-Type: text/plain; charset=utf-8
リンドクヴィスト様
生地のサンプルを本日発送いたしました。来週の木曜日に到着予定です。
佐藤
`,
];
const mbox = messages.map(m => `From sender@example ${new Date().toUTCString()}\n${m.replace(/\r?\n/g, '\r\n')}`).join('\r\n');
document.getElementById('ex-mv-box').addEventListener('click', () => el.open(mbox, 'studio.mbox'));
document.getElementById('ex-mv-one').addEventListener('click', () => el.open(messages[3].replace(/\r?\n/g, '\r\n'), 'notes.eml'));
document.getElementById('ex-mv-threads').addEventListener('change', e => (el.conversations = e.target.checked));
el.addEventListener('bmxOpen', e => (out.textContent = `Opened ${e.detail.name}: ${e.detail.count} message${e.detail.count === 1 ? '' : 's'}.`));
el.addEventListener('bmxLinkOpen', e => {
// The sample's links go nowhere real: a page could open them in a new tab, or handle them itself.
e.preventDefault();
out.textContent = `The message links to ${e.detail.href}.`;
});
el.open(mbox, 'studio.mbox');
</script>
WHAT IT DOES
Shows a message as its sender formatted it, in a sandboxed frame that runs
no script, with pictures from the internet blocked until the reader asks
for them (they tell the sender the message was opened); the pictures the
message carries are shown at once. Or as plain text, or its source.
The people, date, importance, signed or encrypted, a meeting request's
what, when and where; every header on request.
Attachments with their real names in any language: open (pictures,
text, messages attached to messages, files inside winmail.dat), save
one or all as a .zip.
A mailbox: a list read a window at a time, search with from:, to:,
subject:, has:attachment, before: and after:, sorted, or
gathered into conversations.
Save any message as .eml (an Outlook .msg included), copy it as text,
print it or save it as PDF.
Properties
Property
Attribute
Type
Default
Description
allowOpen
allow-open
boolean
true
Show Open and accept dropped files.
compactBelow
compact-below
number
760
Below this width (pixels) the list and the message take turns.
conversations
conversations
boolean
false
Gather a mailbox's messages into conversations.
label
label
string
—
The element's accessible name.
locale
locale
string
—
The locale for dates and sizes. Unset: the page's lang.
remoteImages
remote-images
BmxMailViewerRemote
'ask'
Pictures from the internet: ask, block or allow.
src
src
string
—
A file to open by address (fetched from the page's own server).
strings
strings
Partial<BmxMailViewerStrings> | string
—
Replacements for any of the wording (an object, or its JSON).
view
view
BmxMailViewerView
'html'
How the body is shown.
Events
Event
Detail
Description
bmxAttachmentOpen
BmxMailViewerAttachmentDetail
Fired before an attachment is opened or saved. Cancelable: cancel to handle the file yourself.
bmxError
{ message: string; name?: string | undefined; }
Fired when a file cannot be opened.
bmxLinkOpen
BmxMailViewerLinkDetail
Fired when a link in a message is followed. Cancelable: cancel to handle it yourself.
bmxMessageChange
BmxMailViewerMessageDetail
Fired when another message is shown.
bmxOpen
BmxMailViewerOpenDetail
Fired when files are opened.
Methods
Method
Signature
Description
allowRemote
allowRemote() => Promise<void>
Load the pictures from the internet in the message being shown.
clear
clear() => Promise<void>
Close what is open.
getMailbox
getMailbox() => Promise<BmxMailViewerEntry[]>
The open mailbox's messages (headers only).
getMessage
getMessage() => Promise<BmxMailMessage | null>
The message being shown (an attached one, when one is open).