diff --git a/app/api/dev-jmap/[...path]/route.ts b/app/api/dev-jmap/[...path]/route.ts index 00b2c1572..f4069ded6 100644 --- a/app/api/dev-jmap/[...path]/route.ts +++ b/app/api/dev-jmap/[...path]/route.ts @@ -1773,7 +1773,7 @@ function handleEmailSet(args: MethodArgs, callId: string): MethodResult { from: (data.from as MockEmail['from']) || [{ name: 'Dev User', email: 'dev@localhost' }], to: (data.to as MockEmail['to']) || [], cc: (data.cc as MockEmail['cc']) || [], - subject: (data.subject as string) || '(no subject)', + subject: typeof data.subject === 'string' ? data.subject : '', preview: (previewText || (data.subject as string) || '').slice(0, 120), hasAttachment: false, textBody: [], diff --git a/components/email/__tests__/empty-subject-warning.test.tsx b/components/email/__tests__/empty-subject-warning.test.tsx index 43a55dab4..6d4d09496 100644 --- a/components/email/__tests__/empty-subject-warning.test.tsx +++ b/components/email/__tests__/empty-subject-warning.test.tsx @@ -1,7 +1,8 @@ -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor, act, within } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import React from 'react'; import { EmailComposer } from '../email-composer'; +import { useAuthStore } from '@/stores/auth-store'; // ─── Heavy component mocks (mirrors reply-addressing.test.tsx) ──────────────── @@ -146,6 +147,7 @@ vi.mock('@/lib/plugin-hooks', () => ({ getRecipientSuggestions: { call: async () => [] }, onRecipientChipsChange: { transform: async (chips: unknown) => chips }, onDraftChange: { emit: () => {} }, + onBeforeDraftAutoSave: { transform: async (draft: unknown) => draft }, onBeforeEmailSend: { intercept: async () => true }, onComposeSend: { intercept: async () => true }, onTransformOutgoingEmail: { transform: async (email: unknown) => email }, @@ -272,4 +274,37 @@ describe('composer empty subject warning', () => { await waitFor(() => expect(onSend).toHaveBeenCalledTimes(1)); expect(screen.queryByText('empty_subject.title')).not.toBeInTheDocument(); }); + + // A draft saved with a "(No Subject)" placeholder came back with that as + // its subject, so sending the resumed draft skipped the warning (#1189). + it('saves a draft without a subject as an empty subject', async () => { + const createDraft = vi.fn().mockResolvedValue('draft-8'); + useAuthStore.setState({ + client: { + createDraft, + getEmail: vi.fn().mockResolvedValue({ id: 'draft-8', attachments: [] }), + hasDelayedSend: () => false, + getMaxDelayedSend: () => 0, + } as never, + activeAccountId: 'acct-A' as never, + }); + + const requestCloseRef = { current: null } as React.MutableRefObject<((afterClose?: () => void) => void) | null>; + render( + + ); + fireEvent.change(screen.getByDisplayValue('Lunch'), { target: { value: '' } }); + + act(() => requestCloseRef.current!(vi.fn())); + const dialog = await screen.findByRole('alertdialog'); + fireEvent.click(within(dialog).getByText('save')); + + await waitFor(() => expect(createDraft).toHaveBeenCalled()); + expect(createDraft.mock.calls[0][1]).toBe(''); + useAuthStore.setState({ client: null, activeAccountId: null }); + }); }); diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index afaaee5cb..ab9f926b0 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -2038,7 +2038,10 @@ export function EmailComposer({ const previousDraftId = draftIdRef.current; let savedDraft : AlmostSavedDraft = { to: toAddresses, - subject: subject || t('no_subject'), + // Save an empty subject as empty. A "(No Subject)" placeholder here + // came back as a real subject on reopen, and Send then skipped the + // empty-subject warning (#1189). + subject, body: draftTextBody, cc: ccAddresses, bcc: bccAddresses, diff --git a/components/mail/mail-app.tsx b/components/mail/mail-app.tsx index 677531014..e58501dd4 100644 --- a/components/mail/mail-app.tsx +++ b/components/mail/mail-app.tsx @@ -2039,7 +2039,9 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { to: draft.to?.map(a => a.email).filter(Boolean).join(', ') || '', cc: draft.cc?.map(a => a.email).filter(Boolean).join(', ') || '', bcc: draft.bcc?.map(a => a.email).filter(Boolean).join(', ') || '', - subject: draft.subject || '', + // Drafts saved before #1189 stored an empty subject as the composer's + // "(No Subject)" placeholder; reopen those with an empty field. + subject: draft.subject && draft.subject !== t('email_composer.no_subject') ? draft.subject : '', body: htmlBody || bodyText, // Re-open in the format the draft was written in: a text-only draft // used to land raw in the rich-text editor (newlines collapsed) and an diff --git a/components/pro/pro-email-tab-body.tsx b/components/pro/pro-email-tab-body.tsx index 73b961338..46060b624 100644 --- a/components/pro/pro-email-tab-body.tsx +++ b/components/pro/pro-email-tab-body.tsx @@ -347,7 +347,9 @@ export function ProEmailView({ emailId, client: clientOverride, accountId, onLoa to: email.to?.map((a) => a.email).filter(Boolean).join(', ') || '', cc: email.cc?.map((a) => a.email).filter(Boolean).join(', ') || '', bcc: email.bcc?.map((a) => a.email).filter(Boolean).join(', ') || '', - subject: email.subject || '', + // Drafts saved before #1189 stored an empty subject as the composer's + // "(No Subject)" placeholder; reopen those with an empty field. + subject: email.subject && email.subject !== t('email_composer.no_subject') ? email.subject : '', body: htmlBody || bodyText, showCc: (email.cc?.length || 0) > 0, showBcc: (email.bcc?.length || 0) > 0,