feat: mail phase 2 - delete, move, drafts
- Add deleted_at field to Mail/MailAccount/MailFolder models
- Add DELETE /{mail_id} endpoint (soft-delete + IMAP EXPUNGE)
- Add POST /{mail_id}/move endpoint (IMAP UID MOVE + folder_id update)
- Add POST /drafts and PUT /drafts/{id} endpoints (save/edit drafts)
- Add imap_delete_mail() and imap_move_mail() service functions
- Add save_draft() and update_draft() service functions
- Frontend: deleteMail, moveMail, saveDraft, updateDraft API functions
- ComposeModal: draft mode with Save Draft button
- MailDetail: delete/move buttons, edit-draft for drafts
- Mail.tsx: bulk delete/move, move dropdown, draft handlers
- i18n: 13 new keys (de/en)
This commit is contained in:
@@ -10,10 +10,10 @@ 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';
|
||||
import type { Mail, MailSignature, SendMailPayload, ReplyPayload, ForwardPayload, MailDraftPayload } from '@/api/mail';
|
||||
import { uploadAttachment, type UploadedAttachment } from '@/api/mail';
|
||||
|
||||
export type ComposeMode = 'new' | 'reply' | 'forward';
|
||||
export type ComposeMode = 'new' | 'reply' | 'forward' | 'draft';
|
||||
|
||||
export interface ComposeModalProps {
|
||||
open: boolean;
|
||||
@@ -21,8 +21,10 @@ export interface ComposeModalProps {
|
||||
accountId: string;
|
||||
replyToMail: Mail | null;
|
||||
forwardMail: Mail | null;
|
||||
draftMail?: Mail | null;
|
||||
signatures: MailSignature[];
|
||||
onSend: (payload: SendMailPayload | ReplyPayload | ForwardPayload, mode: ComposeMode) => Promise<void>;
|
||||
onSaveDraft?: (payload: MailDraftPayload) => Promise<void>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -32,8 +34,10 @@ export function ComposeModal({
|
||||
accountId,
|
||||
replyToMail,
|
||||
forwardMail,
|
||||
draftMail,
|
||||
signatures,
|
||||
onSend,
|
||||
onSaveDraft,
|
||||
onClose,
|
||||
}: ComposeModalProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -45,6 +49,7 @@ export function ComposeModal({
|
||||
const [body, setBody] = useState('');
|
||||
const [showCc, setShowCc] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [savingDraft, setSavingDraft] = useState(false);
|
||||
const [selectedSignatureId, setSelectedSignatureId] = useState('');
|
||||
const [showTemplatePicker, setShowTemplatePicker] = useState(false);
|
||||
const [attachments, setAttachments] = useState<UploadedAttachment[]>([]);
|
||||
@@ -61,6 +66,13 @@ export function ComposeModal({
|
||||
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 if (mode === 'draft' && draftMail) {
|
||||
setTo(draftMail.to_addresses.join(', '));
|
||||
setCc(draftMail.cc_addresses.join(', '));
|
||||
setBcc(draftMail.bcc_addresses.join(', '));
|
||||
setSubject(draftMail.subject);
|
||||
setBody(draftMail.body_html || draftMail.body_text || '');
|
||||
setShowCc(draftMail.cc_addresses.length > 0 || draftMail.bcc_addresses.length > 0);
|
||||
} else {
|
||||
setTo('');
|
||||
setCc('');
|
||||
@@ -69,7 +81,7 @@ export function ComposeModal({
|
||||
setBody('');
|
||||
setAttachments([]);
|
||||
}
|
||||
}, [open, mode, replyToMail, forwardMail, t]);
|
||||
}, [open, mode, replyToMail, forwardMail, draftMail, t]);
|
||||
|
||||
const execCommand = useCallback((command: string, value?: string) => {
|
||||
document.execCommand(command, false, value);
|
||||
@@ -142,6 +154,28 @@ export function ComposeModal({
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
};
|
||||
|
||||
const handleSaveDraft = useCallback(async () => {
|
||||
if (!onSaveDraft) return;
|
||||
setSavingDraft(true);
|
||||
try {
|
||||
const toList = to.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
const ccList = cc ? cc.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
||||
const bccList = bcc ? bcc.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
||||
const payload: MailDraftPayload = {
|
||||
account_id: accountId,
|
||||
to: toList,
|
||||
cc: ccList,
|
||||
bcc: bccList,
|
||||
subject,
|
||||
body_text: body.replace(/<[^>]*>/g, ''),
|
||||
body_html: body,
|
||||
};
|
||||
await onSaveDraft(payload);
|
||||
} finally {
|
||||
setSavingDraft(false);
|
||||
}
|
||||
}, [to, cc, bcc, subject, body, accountId, onSaveDraft]);
|
||||
|
||||
const handleSend = useCallback(async () => {
|
||||
if (!to.trim()) return;
|
||||
setSending(true);
|
||||
@@ -190,7 +224,7 @@ export function ComposeModal({
|
||||
}
|
||||
}, [to, cc, bcc, subject, body, mode, replyToMail, forwardMail, accountId, selectedSignatureId, attachments, onSend, onClose]);
|
||||
|
||||
const title = mode === 'reply' ? t('mail.reply') : mode === 'forward' ? t('mail.forward') : t('mail.compose');
|
||||
const title = mode === 'reply' ? t('mail.reply') : mode === 'forward' ? t('mail.forward') : mode === 'draft' ? t('mail.editDraft') : t('mail.compose');
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} title={title} size="xl" closeOnBackdrop={false} fullScreenMobile>
|
||||
@@ -379,6 +413,17 @@ export function ComposeModal({
|
||||
<Button variant="secondary" onClick={onClose} type="button">
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
{onSaveDraft && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleSaveDraft}
|
||||
isLoading={savingDraft}
|
||||
type="button"
|
||||
data-testid="compose-save-draft"
|
||||
>
|
||||
{t('mail.saveDraft')}
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleSend} isLoading={sending} type="button" data-testid="compose-send">
|
||||
{t('mail.send')}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user