Bodies
A body is the decoded content of one message, stored separately from the header record.
Bodies are not materialized during initial sync. They are fetched on demand and cached locally for subsequent reads.
Schema
Stored in the bodies table:
| Column | Type | Notes |
|---|---|---|
message_id |
INTEGER | Primary key, foreign key → messages.id |
text_plain |
TEXT | Decoded text/plain part |
text_html |
TEXT | Decoded text/html part |
raw |
BLOB | Full RFC 2822 message bytes |
The message_id column is both the primary key and the foreign key, enforcing
a strict one-to-one relationship with the messages table. There is no
separate id column.
On-Demand Fetch Model
The MIME body is not fetched during the initial sync pass to keep sync fast and bandwidth-efficient. The workflow is:
read.getMessage(folder, uid)checks whether abodiesrow exists.- If absent, it triggers
network.fetchBody(folder, uid)transparently. - The fetched bytes are decoded via the MIME parser and stored in
bodies. - Subsequent calls to
read.getMessagereturn the cached body immediately.
The same pattern applies to read.getAttachment().
MIME Decoding
The text_plain and text_html columns contain decoded and transfer-decoded
text:
- Quoted-Printable and Base64 Content-Transfer-Encoding are decoded
- RFC 2047 encoded-word headers (=?charset?encoding?text?=) are decoded
- Character set conversion is applied via
TextDecoder
When only text_html is present, text_plain is derived from it via the
htmlToText utility (strips tags, normalises whitespace).
Relationship To body_structure
The body_structure column on the messages row holds the full decoded
IMAP BODYSTRUCTURE response as a BodyPart tree. The bodies row holds the
actual content bytes. Together they give a complete picture of the message
structure without requiring re-parsing.
Local Store Operations
Store functions are accessible through the store namespace:
import { store } from '@ghostpaw/email';
const body = store.upsertBody(db, {
messageId: message.id,
textPlain: 'Hello world',
textHtml: '<p>Hello world</p>',
raw: rawBuffer,
});
const fetched = store.getBody(db, message.id);
Body access through the read surface handles the fetch-on-demand cycle:
// Returns MessageDetail including textPlain, textHtml, and attachment metas.
// Fetches from server if body is not cached.
const detail = await mailbox.read.getMessage('INBOX', uid);
Invariants
- At most one
bodiesrow permessages.id rawis the verbatim RFC 2822 bytes from the IMAP FETCH BODY[] responsetext_plainandtext_htmlmay both be null for messages with no text parts (binary-only messages)- Body rows are deleted when the parent message is deleted (ON DELETE CASCADE)