Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 42 additions & 39 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
155 changes: 155 additions & 0 deletions src/components/compose-message/hooks/useComposeSend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { useCallback, useMemo } from 'react';
import type { Editor } from '@tiptap/react';
import type { EmailAddress, 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';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Replace relative internal import with @/* alias.

Use the project alias for Recipient import to keep module imports consistent.

Proposed patch
-import type { Recipient } from '../types';
+import type { Recipient } from '`@/components/compose-message/types`';

As per coding guidelines, "Use the path alias @/*src/* when importing internal modules."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { Recipient } from '../types';
import type { Recipient } from '`@/components/compose-message/types`';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/compose-message/hooks/useComposeSend.ts` at line 14, Replace
the relative import of the Recipient type with the project path alias; update
the import in useComposeSend.ts that currently imports "Recipient" from
'../types' to use the '`@/`...' alias pointing at the same module (e.g., import
Recipient from '`@/components/compose-message/types`' or the correct aliased path
to that types file) so the file continues to reference the same symbol Recipient
but via the `@/`* alias.


export type EncryptionState = 'none' | '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<void>;
}

/**
* 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();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const allRecipients = useMemo(
() => [...toRecipients, ...ccRecipients, ...bccRecipients],
[toRecipients, ccRecipients, bccRecipients],
);

const encryptionState = useMemo<EncryptionState>(() => {
if (allRecipients.length === 0) return 'none';
if (!activeDomains) return 'none';
return classifyRecipients(
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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;
}

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));
const lookup = await triggerLookup({ addresses: uniqueAddresses }).unwrap();
const usable = lookup.filter((r): r is { address: string; publicKey: string } => Boolean(r.publicKey));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

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;
Loading
Loading