/** * Compose modal — rich text editor with toolbar (bold, italic, link, template insert). * Used for new mail, reply, and forward. */ import React, { useState, useRef, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Modal } from '@/components/ui/Modal'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Select } from '@/components/ui/Select'; import { TemplatePicker } from './TemplatePicker'; import type { Mail, MailSignature, SendMailPayload, ReplyPayload, ForwardPayload } from '@/api/mail'; export type ComposeMode = 'new' | 'reply' | 'forward'; export interface ComposeModalProps { open: boolean; mode: ComposeMode; accountId: string; replyToMail: Mail | null; forwardMail: Mail | null; signatures: MailSignature[]; onSend: (payload: SendMailPayload | ReplyPayload | ForwardPayload, mode: ComposeMode) => Promise; onClose: () => void; } export function ComposeModal({ open, mode, accountId, replyToMail, forwardMail, signatures, onSend, onClose, }: ComposeModalProps) { const { t } = useTranslation(); const editorRef = useRef(null); const [to, setTo] = useState(''); const [cc, setCc] = useState(''); const [bcc, setBcc] = useState(''); const [subject, setSubject] = useState(''); const [body, setBody] = useState(''); const [showCc, setShowCc] = useState(false); const [sending, setSending] = useState(false); const [selectedSignatureId, setSelectedSignatureId] = useState(''); const [showTemplatePicker, setShowTemplatePicker] = useState(false); useEffect(() => { if (!open) return; if (mode === 'reply' && replyToMail) { setTo(replyToMail.from_address); setSubject(replyToMail.subject.startsWith('Re: ') ? replyToMail.subject : `Re: ${replyToMail.subject}`); setBody(`\n\n---\n${replyToMail.body_text.slice(0, 200)}`); } else if (mode === 'forward' && forwardMail) { setTo(''); setSubject(forwardMail.subject.startsWith('Fwd: ') ? forwardMail.subject : `Fwd: ${forwardMail.subject}`); setBody(`\n\n---\n${t('mail.forwarding')}\n${t('mail.from')}: ${forwardMail.from_address}\n${t('mail.subject')}: ${forwardMail.subject}\n\n${forwardMail.body_text.slice(0, 200)}`); } else { setTo(''); setCc(''); setBcc(''); setSubject(''); setBody(''); } }, [open, mode, replyToMail, forwardMail, t]); const execCommand = useCallback((command: string, value?: string) => { document.execCommand(command, false, value); if (editorRef.current) { setBody(editorRef.current.innerHTML); } editorRef.current?.focus(); }, []); const insertLink = useCallback(() => { const url = window.prompt(t('mail.linkUrl')); if (url) { execCommand('createLink', url); } }, [execCommand, t]); const insertSignature = useCallback((signatureId: string) => { setSelectedSignatureId(signatureId); const sig = signatures.find((s) => s.id === signatureId); if (sig && editorRef.current) { const current = editorRef.current.innerHTML; editorRef.current.innerHTML = `${current}

---
${sig.body_html}`; setBody(editorRef.current.innerHTML); } }, [signatures]); const handleTemplateSelect = useCallback((templateBody: string, templateSubject: string) => { if (editorRef.current) { editorRef.current.innerHTML = templateBody; setBody(templateBody); } if (templateSubject) { setSubject(templateSubject); } setShowTemplatePicker(false); }, []); const handleSend = useCallback(async () => { if (!to.trim()) return; setSending(true); try { const toList = to.split(',').map((s) => s.trim()).filter(Boolean); const ccList = cc ? cc.split(',').map((s) => s.trim()).filter(Boolean) : undefined; const bccList = bcc ? bcc.split(',').map((s) => s.trim()).filter(Boolean) : undefined; if (mode === 'reply' && replyToMail) { const replyPayload: ReplyPayload = { account_id: accountId, body, is_html: true, to: toList, cc: ccList, signature_id: selectedSignatureId || null, }; await onSend(replyPayload, 'reply'); } else if (mode === 'forward' && forwardMail) { const fwdPayload: ForwardPayload = { account_id: accountId, to: toList, body, is_html: true, signature_id: selectedSignatureId || null, }; await onSend(fwdPayload, 'forward'); } else { const sendPayload: SendMailPayload = { account_id: accountId, to: toList, cc: ccList, bcc: bccList, subject, body, is_html: true, signature_id: selectedSignatureId || null, }; await onSend(sendPayload, 'new'); } onClose(); } finally { setSending(false); } }, [to, cc, bcc, subject, body, mode, replyToMail, forwardMail, accountId, selectedSignatureId, onSend, onClose]); const title = mode === 'reply' ? t('mail.reply') : mode === 'forward' ? t('mail.forward') : t('mail.compose'); return (
{/* Toolbar */}
{showTemplatePicker && ( )} {/* Recipients */}
setTo(e.target.value)} placeholder="recipient@example.com" required data-testid="compose-to" />
{!showCc ? ( ) : (
setCc(e.target.value)} placeholder="cc@example.com" /> setBcc(e.target.value)} placeholder="bcc@example.com" />
)}
setSubject(e.target.value)} placeholder={t('mail.subjectPlaceholder')} data-testid="compose-subject" />
{/* Editor */}
setBody((e.target as HTMLDivElement).innerHTML)} className="min-h-48 border border-secondary-300 rounded-md p-3 focus:outline-none focus:ring-2 focus:ring-primary-500 overflow-y-auto" role="textbox" aria-multiline="true" aria-label={t('mail.body')} data-testid="compose-editor" dangerouslySetInnerHTML={mode === 'reply' && replyToMail ? { __html: body } : undefined} />
{/* Signature */}