mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
A draft that carried attachments and no message re-opened with the
contents of one of the attachments as its text. Attach a .csv to an
empty draft, save it, re-open it: the editor shows the rows of the csv
as what you had written, and the next save or send makes that the real
message.
Both edit-draft paths built the text body by joining every value in
bodyValues:
Object.values(draft.bodyValues).map(v => v.value).join('\n')
bodyValues is keyed by part id, and getEmail asks for
fetchAllBodyValues, which per RFC 8621 4.1.4 returns a value for every
text/* part of the message - attachments included. So any attached
.csv, .txt, .log or .md was joined in as body text, and with no html
body to prefer it became the whole message.
Take the body from the parts the server lists as the body instead, and
from nothing else. A draft whose textBody is empty now re-opens empty,
which is what an attachments-only draft is.
The selection lives in lib/email-body-selection.ts beside
getRenderableHtmlBody, which exists because the viewer and the mobile
thread view had already drifted apart once (#489). mail-app and the pro
email tab held character-for-character copies of this join, so the same
drift was already in place.
57 lines
2.5 KiB
TypeScript
57 lines
2.5 KiB
TypeScript
import { Email } from '@/lib/jmap/types';
|
|
import { hasMeaningfulHtmlBody } from '@/lib/signature-utils';
|
|
|
|
/**
|
|
* Pick the HTML body an email should be rendered from, or null when it should be
|
|
* rendered as plain text instead. Shared by the desktop viewer and the mobile
|
|
* thread view so the two cannot drift apart again (#489: the thread view was
|
|
* missing the part-type guard and rendered plain-text-only mail as HTML,
|
|
* collapsing every line break).
|
|
*/
|
|
export function getRenderableHtmlBody(
|
|
email: Pick<Email, 'htmlBody' | 'textBody' | 'bodyValues'>
|
|
): string | null {
|
|
const htmlPart = email.htmlBody?.[0];
|
|
if (!htmlPart?.partId || !email.bodyValues?.[htmlPart.partId]) return null;
|
|
|
|
const htmlContent = email.bodyValues[htmlPart.partId].value;
|
|
if (!htmlContent) return null;
|
|
|
|
// Per RFC 8621, a single-part email exposes the same part in both htmlBody and
|
|
// textBody. That shared part may actually be text/plain (plain-text-only mail);
|
|
// rendering it as HTML collapses newlines and skips linkification, so route by
|
|
// the part's media type rather than by which list it appears in.
|
|
if (htmlPart.type && htmlPart.type.toLowerCase() !== 'text/html') return null;
|
|
|
|
// Prefer textBody when the HTML is an auto-generated minimal wrapper (no rich
|
|
// formatting). Server-generated HTML from text/plain emails often lacks <br>
|
|
// tags, which would collapse newlines just the same.
|
|
const textPartId = email.textBody?.[0]?.partId;
|
|
const hasDistinctTextBody =
|
|
!!textPartId && textPartId !== htmlPart.partId && !!email.bodyValues[textPartId];
|
|
if (hasDistinctTextBody && !hasMeaningfulHtmlBody(htmlContent)) return null;
|
|
|
|
return htmlContent;
|
|
}
|
|
|
|
/**
|
|
* The plain-text body of a draft being re-opened, taken from the parts the
|
|
* server lists as the body and from nothing else.
|
|
*
|
|
* `bodyValues` is keyed by part id, and `getEmail` asks for
|
|
* `fetchAllBodyValues`, which per RFC 8621 § 4.1.4 covers every `text/*`
|
|
* part of the message - attachments included. Joining all of them put the
|
|
* contents of an attached .csv or .txt into the composer as the message
|
|
* text: a draft carrying only attachments and no body re-opened showing an
|
|
* attachment's contents as what you had written, and saving or sending it
|
|
* then made that the actual message.
|
|
*/
|
|
export function getDraftTextBody(
|
|
email: Pick<Email, 'textBody' | 'bodyValues'>
|
|
): string {
|
|
return (email.textBody ?? [])
|
|
.map(part => (part.partId ? email.bodyValues?.[part.partId]?.value : undefined))
|
|
.filter((value): value is string => !!value)
|
|
.join('\n');
|
|
}
|