diff --git a/package-lock.json b/package-lock.json
index 04d00e4..5390aaa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,7 @@
"dependencies": {
"@internxt/css-config": "^1.1.0",
"@internxt/lib": "^1.4.1",
- "@internxt/sdk": "^1.16.3",
+ "@internxt/sdk": "1.17.2",
"@internxt/ui": "^0.1.16",
"@phosphor-icons/react": "^2.1.10",
"@reduxjs/toolkit": "^2.11.2",
@@ -31,7 +31,7 @@
"dompurify": "^3.3.3",
"i18next": "^25.8.13",
"idb": "^8.0.3",
- "internxt-crypto": "^1.3.0",
+ "internxt-crypto": "1.4.0",
"prettysize": "^2.0.0",
"react": "^19.2.0",
"react-device-detect": "^2.2.3",
@@ -1453,9 +1453,9 @@
"license": "MIT"
},
"node_modules/@internxt/sdk": {
- "version": "1.16.3",
- "resolved": "https://registry.npmjs.org/@internxt/sdk/-/sdk-1.16.3.tgz",
- "integrity": "sha512-GmX9eYBOBB09wr5e9yW3gUIR3Pn2AgZBXzZd1HvzwS96AonclcEWY1/+uoZ9qLO4SdvYquiOjXfLYNJGg99ugQ==",
+ "version": "1.17.2",
+ "resolved": "https://registry.npmjs.org/@internxt/sdk/-/sdk-1.17.2.tgz",
+ "integrity": "sha512-Wrtacs42uj8It4jq8cMZsSYyNME6swp7B6n0k+WMOGSfh5EkwaaOGtILmlZpLUKl3xEUBJWSSwxkR/qHo4FmLQ==",
"license": "MIT",
"dependencies": {
"axios": "^1.16.0"
@@ -1524,9 +1524,9 @@
}
},
"node_modules/@noble/ciphers": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz",
- "integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.2.0.tgz",
+ "integrity": "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
@@ -1536,12 +1536,12 @@
}
},
"node_modules/@noble/curves": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz",
- "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz",
+ "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==",
"license": "MIT",
"dependencies": {
- "@noble/hashes": "2.0.1"
+ "@noble/hashes": "2.2.0"
},
"engines": {
"node": ">= 20.19.0"
@@ -1551,7 +1551,9 @@
}
},
"node_modules/@noble/hashes": {
- "version": "2.0.1",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
+ "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
@@ -1561,13 +1563,14 @@
}
},
"node_modules/@noble/post-quantum": {
- "version": "0.5.4",
- "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.5.4.tgz",
- "integrity": "sha512-leww0zzIirrvwaYMPI9fj6aRIlA/c6Y0/lifQQ1YOOyHEr0MNH3yYpjXeiVG+tWdPps4XxGclFWX2INPO3Yo5w==",
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.6.1.tgz",
+ "integrity": "sha512-+pormrDZwjRw05U8ADK4JpHejo87+gBd+muRBB/ozztH5yhDLMDF4jHQWN3NQQAsu1zBNPWTG0ZwVI0CR29H0A==",
"license": "MIT",
"dependencies": {
- "@noble/curves": "~2.0.0",
- "@noble/hashes": "~2.0.0"
+ "@noble/ciphers": "~2.2.0",
+ "@noble/curves": "~2.2.0",
+ "@noble/hashes": "~2.2.0"
},
"engines": {
"node": ">= 20.19.0"
@@ -3536,22 +3539,22 @@
]
},
"node_modules/@scure/base": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz",
- "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz",
+ "integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip39": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.0.1.tgz",
- "integrity": "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.2.0.tgz",
+ "integrity": "sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A==",
"license": "MIT",
"dependencies": {
- "@noble/hashes": "2.0.1",
- "@scure/base": "2.0.0"
+ "@noble/hashes": "2.2.0",
+ "@scure/base": "2.2.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
@@ -7068,18 +7071,18 @@
"license": "ISC"
},
"node_modules/internxt-crypto": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/internxt-crypto/-/internxt-crypto-1.3.0.tgz",
- "integrity": "sha512-OEZzwtq+PkPQ8WmMwfqv6+e5iFn77avB6SM/5fV9YgZ6RU5t8R8yqKnXlbi8SKq7VqnxBUwRnh2zPNtky94R3w==",
- "dependencies": {
- "@noble/ciphers": "^2.1.1",
- "@noble/curves": "^2.0.1",
- "@noble/hashes": "^2.0.1",
- "@noble/post-quantum": "^0.5.2",
- "@scure/bip39": "^2.0.1",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/internxt-crypto/-/internxt-crypto-1.4.0.tgz",
+ "integrity": "sha512-4G7M1CBhnkLBbIfMhXpCJA9mTezZfJnleTgRM7Z0iicCXdjlGzBfygRJwlnnhKNvh45pu9hIQ+M5dA0jVIIKzw==",
+ "dependencies": {
+ "@noble/ciphers": "^2.2.0",
+ "@noble/curves": "^2.2.0",
+ "@noble/hashes": "^2.2.0",
+ "@noble/post-quantum": "^0.6.1",
+ "@scure/bip39": "^2.2.0",
"hash-wasm": "^4.12.0",
"husky": "^9.1.7",
- "uuid": "^13.0.0"
+ "uuid": "^14.0.0"
}
},
"node_modules/is-arguments": {
@@ -9841,9 +9844,9 @@
"license": "MIT"
},
"node_modules/uuid": {
- "version": "13.0.0",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
- "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz",
+ "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
diff --git a/package.json b/package.json
index 19cf4a8..52c142c 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
"dependencies": {
"@internxt/css-config": "^1.1.0",
"@internxt/lib": "^1.4.1",
- "@internxt/sdk": "^1.16.3",
+ "@internxt/sdk": "1.17.2",
"@internxt/ui": "^0.1.16",
"@phosphor-icons/react": "^2.1.10",
"@reduxjs/toolkit": "^2.11.2",
@@ -44,7 +44,7 @@
"dompurify": "^3.3.3",
"i18next": "^25.8.13",
"idb": "^8.0.3",
- "internxt-crypto": "^1.3.0",
+ "internxt-crypto": "1.4.0",
"prettysize": "^2.0.0",
"react": "^19.2.0",
"react-device-detect": "^2.2.3",
diff --git a/src/components/compose-message/hooks/useComposeSend.test.ts b/src/components/compose-message/hooks/useComposeSend.test.ts
new file mode 100644
index 0000000..ac4034b
--- /dev/null
+++ b/src/components/compose-message/hooks/useComposeSend.test.ts
@@ -0,0 +1,204 @@
+import { renderHook, act } from '@testing-library/react';
+import { beforeEach, describe, expect, test, vi } from 'vitest';
+import type { Editor } from '@tiptap/react';
+import type { EncryptionBlock } from '@internxt/sdk/dist/mail/types';
+import useComposeSend from './useComposeSend';
+import { MailEncryptionService } from '@/services/mail-encryption';
+import notificationsService from '@/services/notifications';
+import type { Recipient } from '../types';
+
+const mocks = vi.hoisted(() => ({
+ activeDomains: undefined as { domain: string }[] | undefined,
+ senderKeys: undefined as { address: string; publicKey: string } | undefined,
+ triggerLookup: vi.fn(),
+ sendEmail: vi.fn(),
+}));
+
+vi.mock('@/store/api/mail', () => ({
+ useGetActiveDomainsQuery: () => ({ data: mocks.activeDomains }),
+ useGetMailAccountKeysQuery: () => ({ data: mocks.senderKeys }),
+ useLazyLookupRecipientKeysQuery: () => [mocks.triggerLookup],
+ useSendEmailMutation: () => [mocks.sendEmail, { isLoading: false }],
+}));
+
+vi.mock('@/i18n', () => ({ useTranslationContext: () => ({ translate: (key: string) => key }) }));
+
+vi.mock('@/services/notifications', () => ({
+ default: { show: vi.fn() },
+ ToastType: { Success: 'success', Error: 'error', Warning: 'warning', Info: 'info', Loading: 'loading' },
+}));
+
+const editor = { getHTML: () => '
body
', getText: () => 'body' } as unknown as Editor;
+const recipient = (email: string): Recipient => ({ id: email, email });
+const show = vi.mocked(notificationsService.show);
+
+const renderSend = (overrides: Partial[0]> = {}) => {
+ const onSent = vi.fn();
+ const { result } = renderHook(() =>
+ useComposeSend({
+ toRecipients: [],
+ ccRecipients: [],
+ bccRecipients: [],
+ subject: 'Hi',
+ editor,
+ onSent,
+ ...overrides,
+ }),
+ );
+ return { result, onSent };
+};
+
+describe('useComposeSend', () => {
+ beforeEach(() => {
+ vi.restoreAllMocks();
+ mocks.triggerLookup.mockReset();
+ mocks.sendEmail.mockReset();
+ show.mockReset();
+
+ mocks.activeDomains = [{ domain: 'inxt.me' }];
+ mocks.senderKeys = { address: 'me@inxt.me', publicKey: 'sender-pk' };
+ mocks.triggerLookup.mockReturnValue({ unwrap: () => Promise.resolve([]) });
+ mocks.sendEmail.mockReturnValue({ unwrap: () => Promise.resolve({ id: 'mail-1' }) });
+ });
+
+ test('When there are no recipients, then it warns and does not send', async () => {
+ const { result, onSent } = renderSend();
+
+ await act(async () => {
+ await result.current.send();
+ });
+
+ expect(show).toHaveBeenCalledWith(expect.objectContaining({ text: 'errors.mail.noRecipients' }));
+ expect(mocks.sendEmail).not.toHaveBeenCalled();
+ expect(onSent).not.toHaveBeenCalled();
+ });
+
+ test('When the active domains have not resolved, then the send is blocked to avoid a cleartext downgrade', async () => {
+ mocks.activeDomains = undefined;
+
+ const { result, onSent } = renderSend({ toRecipients: [recipient('bob@inxt.me')] });
+
+ expect(result.current.encryptionState).toBe('unknown');
+
+ await act(async () => {
+ await result.current.send();
+ });
+
+ expect(show).toHaveBeenCalledWith(expect.objectContaining({ text: 'errors.mail.encryptionUnavailable' }));
+ expect(mocks.sendEmail).not.toHaveBeenCalled();
+ expect(onSent).not.toHaveBeenCalled();
+ });
+
+ test('When sending encrypted but the sender keys are missing, then it reports a key lookup failure', async () => {
+ mocks.senderKeys = undefined;
+
+ const { result, onSent } = renderSend({ toRecipients: [recipient('bob@inxt.me')] });
+
+ await act(async () => {
+ await result.current.send();
+ });
+
+ expect(show).toHaveBeenCalledWith(expect.objectContaining({ text: 'errors.mail.keyLookupFailed' }));
+ expect(mocks.sendEmail).not.toHaveBeenCalled();
+ expect(onSent).not.toHaveBeenCalled();
+ });
+
+ test('When the recipient key lookup throws, then it reports a key lookup failure rather than a send failure', async () => {
+ mocks.triggerLookup.mockReturnValue({ unwrap: () => Promise.reject(new Error('network')) });
+
+ const { result, onSent } = renderSend({ toRecipients: [recipient('bob@inxt.me')] });
+
+ await act(async () => {
+ await result.current.send();
+ });
+
+ expect(show).toHaveBeenCalledWith(expect.objectContaining({ text: 'errors.mail.keyLookupFailed' }));
+ expect(mocks.sendEmail).not.toHaveBeenCalled();
+ expect(onSent).not.toHaveBeenCalled();
+ });
+
+ test('When some recipients have no key, then it reports a key lookup failure', async () => {
+ mocks.triggerLookup.mockReturnValue({
+ unwrap: () => Promise.resolve([{ address: 'bob@inxt.me', publicKey: null }]),
+ });
+
+ const { result, onSent } = renderSend({ toRecipients: [recipient('bob@inxt.me')] });
+
+ await act(async () => {
+ await result.current.send();
+ });
+
+ expect(show).toHaveBeenCalledWith(expect.objectContaining({ text: 'errors.mail.keyLookupFailed' }));
+ expect(mocks.sendEmail).not.toHaveBeenCalled();
+ expect(onSent).not.toHaveBeenCalled();
+ });
+
+ test('When the send mutation fails, then it reports a send failure and does not close the dialog', async () => {
+ mocks.sendEmail.mockReturnValue({ unwrap: () => Promise.reject(new Error('boom')) });
+
+ const { result, onSent } = renderSend({ toRecipients: [recipient('bob@gmail.com')] });
+
+ expect(result.current.encryptionState).toBe('cleartext');
+
+ await act(async () => {
+ await result.current.send();
+ });
+
+ expect(show).toHaveBeenCalledWith(expect.objectContaining({ text: 'errors.mail.sendFailed' }));
+ expect(onSent).not.toHaveBeenCalled();
+ });
+
+ test('When all recipients are external, then it sends cleartext and closes the dialog', async () => {
+ const { result, onSent } = renderSend({ toRecipients: [recipient('bob@gmail.com')] });
+
+ expect(result.current.encryptionState).toBe('cleartext');
+
+ await act(async () => {
+ await result.current.send();
+ });
+
+ expect(mocks.sendEmail).toHaveBeenCalledWith(
+ expect.objectContaining({
+ to: [{ email: 'bob@gmail.com' }],
+ subject: 'Hi',
+ htmlBody: 'body
',
+ textBody: 'body',
+ }),
+ );
+ expect(onSent).toHaveBeenCalled();
+ });
+
+ test('When all recipients are Internxt, then it encrypts the body and sends with the sender included', async () => {
+ mocks.triggerLookup.mockReturnValue({
+ unwrap: () => Promise.resolve([{ address: 'bob@inxt.me', publicKey: 'bob-pk' }]),
+ });
+ const buildSpy = vi
+ .spyOn(MailEncryptionService.instance, 'buildEncryptionBlock')
+ .mockResolvedValue({
+ version: 'v1',
+ encryptedText: 'ct',
+ encryptedPreview: 'cp',
+ wrappedKeys: [],
+ } as EncryptionBlock);
+
+ const { result, onSent } = renderSend({ toRecipients: [recipient('bob@inxt.me')] });
+
+ expect(result.current.encryptionState).toBe('encrypted');
+
+ await act(async () => {
+ await result.current.send();
+ });
+
+ expect(buildSpy).toHaveBeenCalledWith(
+ { body: 'body
', previewText: 'body' },
+ expect.arrayContaining([
+ { address: 'bob@inxt.me', publicKey: 'bob-pk' },
+ { address: 'me@inxt.me', publicKey: 'sender-pk' },
+ ]),
+ );
+ expect(mocks.sendEmail).toHaveBeenCalledWith(
+ expect.objectContaining({ subject: 'Hi', encryption: expect.objectContaining({ version: 'v1' }) }),
+ );
+ expect(onSent).toHaveBeenCalled();
+ });
+});
diff --git a/src/components/compose-message/hooks/useComposeSend.ts b/src/components/compose-message/hooks/useComposeSend.ts
new file mode 100644
index 0000000..c80d055
--- /dev/null
+++ b/src/components/compose-message/hooks/useComposeSend.ts
@@ -0,0 +1,172 @@
+import { useCallback, useMemo } from 'react';
+import type { Editor } from '@tiptap/react';
+import type { EmailAddress, RecipientKey, SendEmailRequest } from '@internxt/sdk/dist/mail/types';
+import {
+ useGetActiveDomainsQuery,
+ useGetMailAccountKeysQuery,
+ useLazyLookupRecipientKeysQuery,
+ useSendEmailMutation,
+} from '@/store/api/mail';
+import { classifyRecipients, uniqueEmailAddresses } from '@/utils/domain';
+import { MailEncryptionService, type RecipientPublicKey } from '@/services/mail-encryption';
+import notificationsService, { ToastType } from '@/services/notifications';
+import { useTranslationContext } from '@/i18n';
+import type { Recipient } from '../types';
+
+export type EncryptionState = 'none' | 'unknown' | 'encrypted' | 'cleartext';
+
+const toEmailAddress = (r: Recipient): EmailAddress => (r.name ? { name: r.name, email: r.email } : { email: r.email });
+
+interface UseComposeSendParams {
+ toRecipients: Recipient[];
+ ccRecipients: Recipient[];
+ bccRecipients: Recipient[];
+ subject: string;
+ editor: Editor | null;
+ onSent: () => void;
+}
+
+interface UseComposeSendResult {
+ encryptionState: EncryptionState;
+ isSending: boolean;
+ send: () => Promise;
+}
+
+/**
+ * Owns the compose dialog's send pipeline: recipient classification, recipient
+ * key lookup, body/preview encryption and dispatch of the send mutation. Keeps
+ * `ComposeMessageDialog` focused on rendering and wiring callbacks.
+ */
+export const useComposeSend = ({
+ toRecipients,
+ ccRecipients,
+ bccRecipients,
+ subject,
+ editor,
+ onSent,
+}: UseComposeSendParams): UseComposeSendResult => {
+ const { translate } = useTranslationContext();
+
+ const { data: activeDomains } = useGetActiveDomainsQuery();
+ const { data: senderKeys } = useGetMailAccountKeysQuery();
+ const [triggerLookup] = useLazyLookupRecipientKeysQuery();
+ const [sendEmail, { isLoading: isSending }] = useSendEmailMutation();
+
+ const allRecipients = useMemo(
+ () => [...toRecipients, ...ccRecipients, ...bccRecipients],
+ [toRecipients, ccRecipients, bccRecipients],
+ );
+
+ const encryptionState = useMemo(() => {
+ if (allRecipients.length === 0) return 'none';
+ if (!activeDomains) return 'unknown';
+ return classifyRecipients(
+ allRecipients.map((r) => r.email),
+ activeDomains,
+ ).allInternxt
+ ? 'encrypted'
+ : 'cleartext';
+ }, [allRecipients, activeDomains]);
+
+ const send = useCallback(async () => {
+ if (allRecipients.length === 0) {
+ notificationsService.show({
+ text: translate('errors.mail.noRecipients'),
+ type: ToastType.Warning,
+ });
+ return;
+ }
+
+ if (encryptionState === 'unknown') {
+ notificationsService.show({
+ text: translate('errors.mail.encryptionUnavailable'),
+ type: ToastType.Error,
+ });
+ return;
+ }
+
+ const htmlBody = editor?.getHTML() ?? '';
+ const textBody = editor?.getText() ?? '';
+ const cleartextPayload: SendEmailRequest = {
+ to: toRecipients.map(toEmailAddress),
+ cc: ccRecipients.length ? ccRecipients.map(toEmailAddress) : undefined,
+ bcc: bccRecipients.length ? bccRecipients.map(toEmailAddress) : undefined,
+ subject,
+ textBody: textBody || undefined,
+ htmlBody: htmlBody || undefined,
+ };
+
+ try {
+ if (encryptionState === 'encrypted') {
+ if (!senderKeys?.address || !senderKeys.publicKey) {
+ notificationsService.show({
+ text: translate('errors.mail.keyLookupFailed'),
+ type: ToastType.Error,
+ });
+ return;
+ }
+ const uniqueAddresses = uniqueEmailAddresses(allRecipients.map((r) => r.email));
+ let lookup: RecipientKey[];
+ try {
+ lookup = await triggerLookup({ addresses: uniqueAddresses }).unwrap();
+ } catch {
+ notificationsService.show({
+ text: translate('errors.mail.keyLookupFailed'),
+ type: ToastType.Error,
+ });
+ return;
+ }
+ const usable = lookup.filter((r): r is { address: string; publicKey: string } => Boolean(r.publicKey));
+
+ if (usable.length !== uniqueAddresses.length) {
+ notificationsService.show({
+ text: translate('errors.mail.keyLookupFailed'),
+ type: ToastType.Error,
+ });
+ return;
+ }
+
+ const recipientsWithKeys: RecipientPublicKey[] = [
+ ...usable,
+ { address: senderKeys.address, publicKey: senderKeys.publicKey },
+ ];
+ const encryption = await MailEncryptionService.instance.buildEncryptionBlock(
+ { body: htmlBody || textBody, previewText: textBody },
+ recipientsWithKeys,
+ );
+ await sendEmail({
+ to: toRecipients.map(toEmailAddress),
+ cc: ccRecipients.length ? ccRecipients.map(toEmailAddress) : undefined,
+ bcc: bccRecipients.length ? bccRecipients.map(toEmailAddress) : undefined,
+ subject,
+ encryption,
+ }).unwrap();
+ } else {
+ await sendEmail(cleartextPayload).unwrap();
+ }
+ onSent();
+ } catch {
+ notificationsService.show({
+ text: translate('errors.mail.sendFailed'),
+ type: ToastType.Error,
+ });
+ }
+ }, [
+ allRecipients,
+ editor,
+ toRecipients,
+ ccRecipients,
+ bccRecipients,
+ subject,
+ encryptionState,
+ senderKeys,
+ triggerLookup,
+ sendEmail,
+ onSent,
+ translate,
+ ]);
+
+ return { encryptionState, isSending, send };
+};
+
+export default useComposeSend;
diff --git a/src/components/compose-message/index.tsx b/src/components/compose-message/index.tsx
index 00d022a..70f5311 100644
--- a/src/components/compose-message/index.tsx
+++ b/src/components/compose-message/index.tsx
@@ -1,4 +1,4 @@
-import { PaperclipIcon, XIcon } from '@phosphor-icons/react';
+import { LockKeyIcon, PaperclipIcon, WarningIcon, XIcon } from '@phosphor-icons/react';
import { useCallback } from 'react';
import type { Recipient } from './types';
import { RecipientInput } from './components/RecipientInput';
@@ -8,6 +8,7 @@ import { EditorBar } from './components/editorBar';
import { ActionDialog, useActionDialog } from '@/context/dialog-manager';
import { useTranslationContext } from '@/i18n';
import useComposeMessage from './hooks/useComposeMessage';
+import useComposeSend from './hooks/useComposeSend';
import { useEditor } from '@tiptap/react';
import { EDITOR_CONFIG } from './config';
@@ -49,11 +50,14 @@ export const ComposeMessageDialog = () => {
onComposeMessageDialogClose(ActionDialog.ComposeMessage);
}, [onComposeMessageDialogClose]);
- const handlePrimaryAction = useCallback(() => {
- const html = editor?.getHTML();
- console.log('html', html);
- onClose();
- }, [editor, onClose]);
+ const { encryptionState, isSending, send } = useComposeSend({
+ toRecipients,
+ ccRecipients,
+ bccRecipients,
+ subject: subjectValue,
+ editor,
+ onSent: onClose,
+ });
if (!editor) return null;
@@ -100,7 +104,7 @@ export const ComposeMessageDialog = () => {
showBccButton={!showBcc}
ccButtonText={translate('modals.composeMessageDialog.cc')}
bccButtonText={translate('modals.composeMessageDialog.bcc')}
- disabled={false}
+ disabled={isSending}
/>
{showCc && (
{
recipients={ccRecipients}
onAddRecipient={(email) => onAddCcRecipient?.(email)}
onRemoveRecipient={(id) => onRemoveCcRecipient?.(id)}
- disabled={false}
+ disabled={isSending}
/>
)}
{showBcc && (
@@ -117,28 +121,46 @@ export const ComposeMessageDialog = () => {
recipients={bccRecipients}
onAddRecipient={(email) => onAddBccRecipient?.(email)}
onRemoveRecipient={(id) => onRemoveBccRecipient?.(id)}
- disabled={false}
+ disabled={isSending}
/>
)}
-
+
{/* !TODO: Handle attachments */}
-
-