diff --git a/app/admin/policy/page.tsx b/app/admin/policy/page.tsx index 24b1744a..427e4cc1 100644 --- a/app/admin/policy/page.tsx +++ b/app/admin/policy/page.tsx @@ -32,6 +32,7 @@ const RESTRICTABLE_SETTINGS = [ { key: 'externalContentPolicy', label: 'External Content Policy', category: 'Email', type: 'enum', allowedValues: ['allow', 'block', 'ask'] }, { key: 'sendConfirmation', label: 'Send Confirmation', category: 'Composer', type: 'boolean' }, { key: 'defaultReplyMode', label: 'Default Reply Mode', category: 'Composer', type: 'enum', allowedValues: ['reply', 'reply-all'] }, + { key: 'plainTextMode', label: 'Plain Text Only', category: 'Composer', type: 'boolean' }, { key: 'sessionTimeout', label: 'Session Timeout', category: 'Privacy', type: 'number' }, { key: 'emailNotificationsEnabled', label: 'Email Notifications', category: 'Notifications', type: 'boolean' }, { key: 'calendarNotificationsEnabled', label: 'Calendar Notifications', category: 'Notifications', type: 'boolean' }, diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index 5de479d8..5a452745 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -99,6 +99,7 @@ export function EmailComposer({ const t = useTranslations('email_composer'); const tCommon = useTranslations('common'); const timeFormat = useSettingsStore((state) => state.timeFormat); + const plainTextMode = useSettingsStore((state) => state.plainTextMode); // Initialize with reply/forward data if provided const getInitialTo = () => { @@ -134,6 +135,26 @@ export function EmailComposer({ }; const getInitialBody = () => { + if (plainTextMode) { + // Plain text mode: produce plain text body with no HTML + const prefix = initialDraftText || ""; + if (!replyTo?.body && !replyTo?.htmlBody) return prefix; + + const date = replyTo.receivedAt ? formatDateTime(replyTo.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) : ""; + const from = replyTo.from?.[0]; + const fromStr = from ? `${from.name || from.email}` : tCommon('unknown'); + + const originalText = replyTo.body || (replyTo.htmlBody ? htmlToPlainText(replyTo.htmlBody) : ''); + const quotedText = originalText.split('\n').map(line => `> ${line}`).join('\n'); + + if (mode === 'forward') { + return `${prefix}\n\n---------- Forwarded message ----------\nFrom: ${fromStr}\nDate: ${date}\nSubject: ${replyTo.subject || ''}\n\n${originalText}`; + } else if (mode === 'reply' || mode === 'replyAll') { + return `${prefix}\n\nOn ${date}, ${fromStr} wrote:\n${quotedText}`; + } + return prefix; + } + const prefix = initialDraftText ? `

${initialDraftText.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '
')}

` : ""; if (!replyTo?.body && !replyTo?.htmlBody) return prefix; @@ -369,12 +390,14 @@ export function EmailComposer({ ? substitutePlaceholders(template.body, filledValues) : template.body; - // Convert template plain text body to HTML for the rich text editor - const htmlBody = `

${filledBody.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '
')}

`; + // In plain text mode, use template body as-is; otherwise convert to HTML + const bodyContent = plainTextMode + ? filledBody + : `

${filledBody.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '
')}

`; if (mode === 'compose') { setSubject(filledSubject); - setBody(htmlBody); + setBody(bodyContent); if (template.defaultRecipients?.to?.length) { setTo(template.defaultRecipients.to.join(', ') + ', '); } @@ -387,7 +410,7 @@ export function EmailComposer({ setShowBcc(true); } } else { - setBody((prev) => htmlBody + prev); + setBody((prev) => bodyContent + (plainTextMode ? '\n' : '') + prev); } if (template.identityId) { @@ -395,7 +418,7 @@ export function EmailComposer({ } setShowTemplatePicker(false); - }, [mode]); + }, [mode, plainTextMode]); useEffect(() => { const handleTemplateKey = (e: KeyboardEvent) => { @@ -530,7 +553,7 @@ export function EmailComposer({ const ccAddresses = cc.split(",").map(e => e.trim()).filter(Boolean); const bccAddresses = bcc.split(",").map(e => e.trim()).filter(Boolean); - if (!toAddresses.length && !subject && !htmlToPlainText(body).trim()) { + if (!toAddresses.length && !subject && !(plainTextMode ? body.trim() : htmlToPlainText(body).trim())) { return null; } @@ -566,7 +589,7 @@ export function EmailComposer({ const savedDraftId = await client.createDraft( toAddresses, subject || t('no_subject'), - htmlToPlainText(body), + plainTextMode ? body : htmlToPlainText(body), ccAddresses, bccAddresses, currentIdentity?.id, @@ -630,7 +653,7 @@ export function EmailComposer({ }, []); const toAddresses = to.split(",").map(e => e.trim()).filter(Boolean); - const bodyPlainText = htmlToPlainText(body).trim(); + const bodyPlainText = plainTextMode ? body.trim() : htmlToPlainText(body).trim(); const hasContent = bodyPlainText || attachments.some(att => att.blobId && !att.uploading); const canSend = toAddresses.length > 0 && !!subject && hasContent; @@ -680,8 +703,8 @@ export function EmailComposer({ : currentIdentity.email : undefined; - // Body is already HTML from the rich text editor. - // Build HTML signature block + // Body is already HTML from the rich text editor (or plain text in plain text mode). + // Build HTML signature block (used only in rich text mode) const buildSignatureHtml = (): string => { if (currentIdentity?.htmlSignature) { return `

--
${sanitizeEmailHtml(currentIdentity.htmlSignature)}`; @@ -692,13 +715,14 @@ export function EmailComposer({ return ''; }; - const signatureHtml = buildSignatureHtml(); + // In plain text mode, send text/plain only (no HTML body) + const finalBody = plainTextMode + ? appendPlainTextSignature(body, currentIdentity) + : appendPlainTextSignature(htmlToPlainText(body), currentIdentity); - // Build final HTML body: editor content + signature - const finalHtmlBody = `
${body}
${signatureHtml}`; - - // Generate plain text version from the HTML body for multipart/alternative - const finalBody = appendPlainTextSignature(htmlToPlainText(body), currentIdentity); + const finalHtmlBody = plainTextMode + ? undefined + : `
${body}
${buildSignatureHtml()}`; try { // S/MIME send pipeline: build raw MIME → sign → encrypt → sendRawEmail @@ -1101,24 +1125,47 @@ export function EmailComposer({ - {/* Body - Rich Text Editor */} - { - setBody(html); - if (validationErrors.body) setValidationErrors(prev => ({ ...prev, body: false })); - }} - onImageUpload={handleImageUpload} - placeholder={t('body_placeholder')} - hasError={validationErrors.body} - /> + {/* Body */} + {plainTextMode ? ( +