Reading Full HTML Email Locally via Thunderbird's mbox Files¶
Why this matters¶
The Gmail MCP connector returns message bodies as plaintext, with HTML stripped. That's fine for most triage, but it breaks down for anything where the formatting carries real information — a receipt's line-item table, a styled newsletter, an email where the plaintext fallback is garbled or missing entirely. There's no way to ask the connector for the original HTML.
Thunderbird, run as a local mail client alongside the Gmail/IMAP connectors, keeps a
complete local copy of every synced mailbox in the standard Unix mbox format — one flat
file per folder, every message in full (headers, plaintext part, HTML part, attachments
encoded inline). Parsing that file directly gets you the real HTML body with no connector
limitation in the way.
Where the files live¶
Thunderbird lays out one mbox file per IMAP folder, named after the folder, with no
extension:
macOS:
Windows / WSL2 (profile lives on the Windows side even when accessed from WSL):
/mnt/c/Users/<user>/AppData/Roaming/Thunderbird/Profiles/<profile-id>/ImapMail/<server>/<FolderName>
The <server> directory is named after the IMAP host (imap.gmail.com, or a self-hosted
mail server's hostname). Every label/folder Gmail exposes over IMAP shows up as a sibling
file in that same directory (INBOX, plus whatever else is subscribed — Bills,
Extra-Life, etc. for a labeled Gmail setup).
The profile ID is a random string Thunderbird generates on first run. If it's not already
known, ls the Profiles/ directory once to find it — there's normally only one.
Reading a message¶
Python's standard-library mailbox module parses the format directly; no dependencies
needed.
import mailbox
from html.parser import HTMLParser
class TextExtractor(HTMLParser):
"""Strip tags and pull plain text out of an HTML email body."""
def __init__(self):
super().__init__()
self.text = []
self.skip = False
def handle_starttag(self, tag, attrs):
if tag in ('style', 'script'):
self.skip = True
def handle_endtag(self, tag):
if tag in ('style', 'script'):
self.skip = False
def handle_data(self, data):
if not self.skip and data.strip():
self.text.append(data.strip())
mbox = mailbox.mbox("/path/to/INBOX")
for msg in mbox:
if 'search term' in msg.get('subject', ''):
for part in msg.walk():
if part.get_content_type() == 'text/html':
html = part.get_payload(decode=True).decode('utf-8', errors='replace')
parser = TextExtractor()
parser.feed(html)
print(' '.join(parser.text))
Strip <style> and <script> before extracting text. Most newsletter and receipt
HTML inlines its CSS directly in a <style> block at the top of the body — without
filtering it out first, the extracted text starts with several hundred characters of raw
CSS rules instead of the actual message content.
mailbox.mbox iterates the whole file in message order; for a large INBOX, filter by
subject/date/sender as early as possible in the loop rather than materializing everything.
The one real constraint¶
Thunderbird has to be open and have synced recently for the mbox to be current. It's a local cache, not a live view — a message that arrived after the last sync simply isn't in the file yet. This makes the technique a good fit for "pull up that email I already have open/already synced" but not for "check if anything new just came in" (use the Gmail/IMAP connector for that, then fall back to mbox parsing only once the message needs its full HTML read).
Related¶
- marcus-mailbox-credential-locations — where mailbox credentials for the fleet are stored