Skip to content

Commit da5251e

Browse files
pal-hexraysclaude
andcommitted
fix(gmail): preserve original HTML styling in PDF export
Inject email header into existing HTML structure instead of wrapping in custom template. Properly handles full HTML documents by injecting after <body> tag. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 18f5e0a commit da5251e

1 file changed

Lines changed: 31 additions & 20 deletions

File tree

src/commands/gmail.ts

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ import { CliError, handleError } from '../utils/errors';
1212
import { readStdin, resolveProfileName } from '../utils/stdin';
1313
import type { GmailAttachment } from '../types/gmail';
1414

15+
function escapeHtml(text: string): string {
16+
return text
17+
.replace(/&/g, '&amp;')
18+
.replace(/</g, '&lt;')
19+
.replace(/>/g, '&gt;')
20+
.replace(/"/g, '&quot;');
21+
}
22+
1523
async function getGmailClient(profileName?: string): Promise<{ client: GmailClient; profile: string }> {
1624
const { tokens, profile } = await getValidTokens('gmail', profileName);
1725
const auth = createGoogleAuth(tokens);
@@ -288,30 +296,33 @@ Query Syntax Examples:
288296
const { client } = await getGmailClient(options.profile);
289297
const message = await client.get(messageId, 'html');
290298

291-
// Build HTML document with email metadata
292-
const html = `<!DOCTYPE html>
299+
// Build HTML document - inject header before body content
300+
const emailHeader = `
301+
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 16px 20px; margin-bottom: 16px; border-bottom: 1px solid #ddd; background: #f9f9f9;">
302+
<div style="font-size: 1.3em; font-weight: 600; margin-bottom: 12px;">${escapeHtml(message.subject)}</div>
303+
<div style="margin: 4px 0; font-size: 0.9em;"><strong>From:</strong> ${escapeHtml(message.from)}</div>
304+
<div style="margin: 4px 0; font-size: 0.9em;"><strong>To:</strong> ${escapeHtml(message.to.join(', '))}</div>
305+
<div style="margin: 4px 0; font-size: 0.9em;"><strong>Date:</strong> ${escapeHtml(message.date)}</div>
306+
</div>`;
307+
308+
let html: string;
309+
const body = message.body || '';
310+
311+
// Check if body is already a full HTML document
312+
if (body.trim().toLowerCase().startsWith('<!doctype') || body.trim().toLowerCase().startsWith('<html')) {
313+
// Inject header after <body> tag
314+
html = body.replace(/<body[^>]*>/i, (match) => `${match}${emailHeader}`);
315+
} else {
316+
// Wrap fragment in minimal HTML
317+
html = `<!DOCTYPE html>
293318
<html>
294-
<head>
295-
<meta charset="utf-8">
296-
<style>
297-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 40px; line-height: 1.5; }
298-
.header { border-bottom: 1px solid #ddd; padding-bottom: 16px; margin-bottom: 24px; }
299-
.header-field { margin: 4px 0; }
300-
.header-label { font-weight: 600; color: #555; }
301-
.subject { font-size: 1.4em; font-weight: 600; margin-bottom: 16px; }
302-
.body { white-space: pre-wrap; }
303-
</style>
304-
</head>
319+
<head><meta charset="utf-8"></head>
305320
<body>
306-
<div class="header">
307-
<div class="subject">${escapeHtml(message.subject)}</div>
308-
<div class="header-field"><span class="header-label">From:</span> ${escapeHtml(message.from)}</div>
309-
<div class="header-field"><span class="header-label">To:</span> ${escapeHtml(message.to)}</div>
310-
<div class="header-field"><span class="header-label">Date:</span> ${escapeHtml(message.date)}</div>
311-
</div>
312-
<div class="body">${message.body}</div>
321+
${emailHeader}
322+
<div style="padding: 0 20px;">${body}</div>
313323
</body>
314324
</html>`;
325+
}
315326

316327
// Launch browser and generate PDF
317328
console.error('Launching browser...');

0 commit comments

Comments
 (0)