From d5cd3c6e409183e7e72e0b7c48eb732a27df4b31 Mon Sep 17 00:00:00 2001 From: Jacob Zeitz Date: Sat, 31 Oct 2026 14:15:06 +0100 Subject: [PATCH] feat(plugins): star and after-send hooks name the mail's account Message ids repeat across accounts, so a hook payload naming only an emailId could not tell a plugin which mailbox the message lies in when it is a shared or group account's. onBeforeEmailStarToggle and onEmailStarToggle now carry accountId when the starred message is not in the user's own account, as the message-row-actions click context does. Every star (the row's button, the reader's star, the key, the menu) goes through toggleStar, one message at a time, so one accountId names it; the account is resolved before the plugins are asked. An own message's payload is unchanged. onAfterEmailSend now carries accountId, the JMAP account the sent message lies in (a mail sent from a shared or group identity lies in that account), so api.email.get finds it. sendEmail returns that account as emailAccountId, the S/MIME send too. The hook also carries composerId, the id the sending composer's slots and actions gave plugins, so a plugin can act on what the user chose in that composer. --- .../__tests__/composer-send-pipeline.test.tsx | 11 ++++++ components/email/email-composer.tsx | 3 ++ .../mail/__tests__/after-send-hook.test.tsx | 8 ++-- components/mail/mail-app.tsx | 1 + .../jmap-send-sender-address.test.ts | 15 +++++++ lib/__tests__/plugin-after-send.test.ts | 14 ++++++- lib/jmap/client.ts | 8 ++-- lib/jmap/types.ts | 6 +++ lib/plugin-after-send.ts | 12 ++++++ lib/plugin-hook-catalog.ts | 6 +-- .../email-store-plugin-organize-hooks.test.ts | 39 +++++++++++++++++++ stores/email-store.ts | 14 ++++--- 12 files changed, 120 insertions(+), 17 deletions(-) diff --git a/components/email/__tests__/composer-send-pipeline.test.tsx b/components/email/__tests__/composer-send-pipeline.test.tsx index 075421c0e..ac68f45e5 100644 --- a/components/email/__tests__/composer-send-pipeline.test.tsx +++ b/components/email/__tests__/composer-send-pipeline.test.tsx @@ -3,6 +3,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import React from 'react'; import { EmailComposer } from '../email-composer'; import { registerOutgoingFinalizer, registerPreSendCheck, resetSendPipeline } from '@/lib/outgoing/send-pipeline'; +import { composerHandle } from '@/lib/plugin-sandbox/composer-registry'; // ─── Heavy component mocks (mirrors composer-reopened-draft-signature.test.tsx) ─ @@ -231,6 +232,16 @@ describe('composer send pipeline', () => { })]); }); + it('names the composer plugins know it by, for onAfterEmailSend', async () => { + let open = false; + const onSend = vi.fn((data: { composerId?: string }) => { open = !!composerHandle(data.composerId); }); + render(); + clickSend(); + await waitFor(() => expect(onSend).toHaveBeenCalledTimes(1)); + expect(onSend.mock.calls[0][0]).toEqual(expect.objectContaining({ composerId: expect.any(String) })); + expect(open).toBe(true); + }); + it('sends nothing when a check says no, and sends after it says yes', async () => { let allow = false; const check = vi.fn(() => allow); diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index bc8f02382..35b24f6e1 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -296,6 +296,8 @@ interface EmailComposerProps { requireTls?: boolean; /** Importance / X-Priority; absent for normal (#970). */ priority?: MessagePriority; + /** The id plugins know this composer by, for onAfterEmailSend. */ + composerId?: string; }) => void | false | Promise; onScheduledSendCreated?: () => void | Promise; onClose?: () => void; @@ -3667,6 +3669,7 @@ export function EmailComposer({ requireTls: requireTls || undefined, priority: priority !== 'normal' ? priority : undefined, delayedUntil: effectiveDelayedUntil, + composerId: pluginComposerIdRef.current ?? undefined, }); // Not when nothing went: no host, or one that sent nothing. if (draftElsewhere && onSend && outcome !== false) await removeDraftVersion(finalDraftId!, draftLogin, draftAccountAtSend); diff --git a/components/mail/__tests__/after-send-hook.test.tsx b/components/mail/__tests__/after-send-hook.test.tsx index 62a03e54a..e71b1006a 100644 --- a/components/mail/__tests__/after-send-hook.test.tsx +++ b/components/mail/__tests__/after-send-hook.test.tsx @@ -25,7 +25,7 @@ const sendData = vi.hoisted(() => ({ delayedUntil: undefined as string | undefin vi.mock('@/components/email/email-composer', () => ({ EmailComposer: ({ onSend }: { onSend?: (data: unknown) => Promise }) => (