feat: iframe-based email rendering with smart dark mode support
- Render HTML emails in sandboxed iframe (srcdoc) for true-to-life display with complete CSS isolation from app styles - Detect emails with native dark mode (prefers-color-scheme) and let them handle their own theming - Apply CSS filter inversion for dark mode on emails without native support, with re-inversion for images/media to preserve appearance - Add per-email light/dark toggle button (Sun/Moon icon) next to email size, resets on email change (not persisted) - Fix HTML reply/forward to include original email HTML content - Send replies as multipart/alternative (text + HTML) - Add drag-and-drop file attachments with overlay indicator - Auto-resize composer textarea to avoid double scrolling - Pin attachments section and bottom toolbar outside scroll area - Collapsible attachment list (show 3, toggle for more)
This commit is contained in:
+27
-14
@@ -1379,7 +1379,8 @@ export class JMAPClient {
|
||||
identityId?: string,
|
||||
fromEmail?: string,
|
||||
draftId?: string,
|
||||
fromName?: string
|
||||
fromName?: string,
|
||||
htmlBody?: string
|
||||
): Promise<void> {
|
||||
const emailId = draftId || `draft-${Date.now()}`;
|
||||
const mailboxes = await this.getMailboxes();
|
||||
@@ -1422,21 +1423,33 @@ export class JMAPClient {
|
||||
create: { "1": { emailId: draftId, identityId: finalIdentityId } },
|
||||
}, "1"]);
|
||||
} else {
|
||||
// Build email body parts - include HTML if available
|
||||
const emailCreate: Record<string, unknown> = {
|
||||
from: [{ ...(fromName ? { name: fromName } : {}), email: fromEmail || this.username }],
|
||||
to: to.map(email => ({ email })),
|
||||
cc: cc?.map(email => ({ email })),
|
||||
bcc: bcc?.map(email => ({ email })),
|
||||
subject,
|
||||
keywords: { "$seen": true },
|
||||
mailboxIds: { [sentMailbox.id]: true },
|
||||
};
|
||||
|
||||
if (htmlBody) {
|
||||
// Send as multipart/alternative with both text and HTML
|
||||
emailCreate.bodyValues = {
|
||||
"text": { value: body },
|
||||
"html": { value: htmlBody },
|
||||
};
|
||||
emailCreate.textBody = [{ partId: "text" }];
|
||||
emailCreate.htmlBody = [{ partId: "html" }];
|
||||
} else {
|
||||
emailCreate.bodyValues = { "1": { value: body } };
|
||||
emailCreate.textBody = [{ partId: "1" }];
|
||||
}
|
||||
|
||||
methodCalls.push(["Email/set", {
|
||||
accountId: this.accountId,
|
||||
create: {
|
||||
[emailId]: {
|
||||
from: [{ ...(fromName ? { name: fromName } : {}), email: fromEmail || this.username }],
|
||||
to: to.map(email => ({ email })),
|
||||
cc: cc?.map(email => ({ email })),
|
||||
bcc: bcc?.map(email => ({ email })),
|
||||
subject,
|
||||
keywords: { "$seen": true },
|
||||
mailboxIds: { [sentMailbox.id]: true },
|
||||
bodyValues: { "1": { value: body } },
|
||||
textBody: [{ partId: "1" }],
|
||||
},
|
||||
},
|
||||
create: { [emailId]: emailCreate },
|
||||
}, "0"]);
|
||||
methodCalls.push(["EmailSubmission/set", {
|
||||
accountId: this.accountId,
|
||||
|
||||
Reference in New Issue
Block a user