Skip to content

[PB-6479]: feature/encrypt email for externals#61

Open
xabg2 wants to merge 1 commit into
masterfrom
feature/send-mail-to-external-accounts
Open

[PB-6479]: feature/encrypt email for externals#61
xabg2 wants to merge 1 commit into
masterfrom
feature/send-mail-to-external-accounts

Conversation

@xabg2

@xabg2 xabg2 commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Support for encrypting emails sent to external recipients using the server's public key. With this approach, the server acts as the recipient of the encrypted payload, receiving the encrypted email and attachments, decrypting them on the server side, and then delivering the content to the final recipients.

Currently, when an email includes both external recipients and Internxt users, the backend sends the email through the external-recipient flow, meaning it is processed as encrypted for all recipients. In a future improvement, recipients should be grouped by type (external recipients vs. Internxt users) so that each group can receive the email through the appropriate delivery mechanism: server-side processing for external recipients and the standard Internxt flow for Internxt users.

@xabg2 xabg2 requested a review from jzunigax2 June 9, 2026 12:34
@xabg2 xabg2 self-assigned this Jun 9, 2026
@xabg2 xabg2 added the enhancement New feature or request label Jun 9, 2026
@sonarqubecloud

sonarqubecloud Bot commented Jun 9, 2026

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR introduces support for encrypted delivery to external mail recipients by distinguishing Internxt users (using their public keys) from external recipients (using a server public key). The EncryptionState type is renamed from encrypted/cleartext to internxt/external, the send pipeline always builds encryption blocks while setting differentiated delivery modes, and comprehensive tests validate both paths.

Changes

External Recipients Encryption Support

Layer / File(s) Summary
Configuration and EncryptionState contract
src/types/config/index.ts, src/services/config/index.ts, .env.example, src/components/compose-message/hooks/useComposeSend.ts
SERVER_PUBLIC_KEY configuration variable is wired into the config contract, and EncryptionState type is updated from `'encrypted'
Compose send logic refactor
src/components/compose-message/hooks/useComposeSend.ts
Send callback now always builds encrypted payloads, substituting SERVER_PUBLIC_KEY for external recipients lacking a public key, and sets deliveryMode to INTERNXT vs EXTERNAL; error handling includes toasts for missing SERVER_PUBLIC_KEY and specific encryption unavailability warnings.
UI badge and network decryption updates
src/components/compose-message/index.tsx, src/services/network/index.ts
Compose dialog badges now render based on internxt/external EncryptionState values; network download introduces conditional decryption that only decrypts payload when attachmentsSessionKey is present.
Test coverage and mocking
src/components/compose-message/hooks/useComposeSend.test.ts
Test fixture mockEncryptionBlock added; existing error-path tests renamed for clarity; Internxt recipient test updated to assert internxt state and encrypted delivery; new external recipients test suite validates SERVER_PUBLIC_KEY fallback behavior and missing-configuration warnings.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly addresses the main feature: encrypting emails for external recipients, which is the core purpose of this changeset.
Description check ✅ Passed The description clearly explains the feature's functionality, implementation approach, and notes future improvements, all relevant to the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/send-mail-to-external-accounts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/compose-message/hooks/useComposeSend.test.ts (1)

86-99: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add blank-line separation between AAA sections.

As per coding guidelines, tests should use blank-line separation between Arrange, Act, and Assert sections. This improves readability by visually distinguishing test phases.

Example for lines 86-99:

♻️ Proposed refactor
 test('When the active domains have not resolved, then the send is blocked', 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();
 });

Apply similar blank-line separation to all test cases in this file.

Also applies to: 101-112, 127-142, 144-171, 189-222, 224-241

🤖 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.test.ts` around lines 86
- 99, Update the test "When the active domains have not resolved, then the send
is blocked" (and the other listed tests) to insert a blank line between the
Arrange, Act and Assert sections so each phase is visually separated: e.g.
separate the setup (mocks.activeDomains = undefined; const { result, onSent } =
renderSend(...); expect(result.current.encryptionState)...), the act (await
act(async () => { await result.current.send(); });), and the assertions
(expect(show)... expect(mocks.sendEmail)... expect(onSent)...). Apply the same
blank-line separation to the other tests referenced (lines 101-112, 127-142,
144-171, 189-222, 224-241) so Arrange/Act/Assert blocks are clearly separated
around calls to renderSend, act, send, and subsequent expect assertions.

Source: Coding guidelines

src/services/network/index.ts (1)

40-53: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the optional-decryption behavior explicit in the method contract.

At Line 46, attachmentsSessionKey is required (Uint8Array), so Line 50’s guard is ineffective for typed callers and decryption still always executes. If this path must support non-encrypted payloads, the parameter should be optional and decryption should only run when a key is actually provided.

Suggested patch
   async download({
     mailId,
     blobId,
     name,
     type,
     attachmentsSessionKey,
   }: {
     mailId: string;
     blobId: string;
     name: string;
     type: string;
-    attachmentsSessionKey: Uint8Array;
+    attachmentsSessionKey?: Uint8Array;
   }): Promise<{ blob: Blob; name: string }> {
     const { data, contentType, fileName } = await MailService.instance.downloadAttachment(mailId, blobId, name, type);
     let payload = new Uint8Array(data) as BlobPart;
-    if (attachmentsSessionKey) {
+    if (attachmentsSessionKey?.length) {
       payload = (await decryptSymmetrically(attachmentsSessionKey, new Uint8Array(data))) as BlobPart;
     }
     const blob = new Blob([payload], { type: contentType || type });
🤖 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/services/network/index.ts` around lines 40 - 53, The
attachmentsSessionKey parameter is currently typed as required Uint8Array but
the code conditionally checks it, so change the method signature/destructured
param type to attachmentsSessionKey?: Uint8Array (or Uint8Array | undefined) and
update any callers to pass undefined when no key is available; keep the guard if
(attachmentsSessionKey) before calling decryptSymmetrically so decryption only
runs when a key is provided, and ensure payload is constructed from either the
raw data or the decrypted result (use new Uint8Array(data) for the non-encrypted
path and the decrypted Uint8Array for the encrypted path) while preserving the
returned Blob creation and types.
🤖 Prompt for all review comments with 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.

Outside diff comments:
In `@src/components/compose-message/hooks/useComposeSend.test.ts`:
- Around line 86-99: Update the test "When the active domains have not resolved,
then the send is blocked" (and the other listed tests) to insert a blank line
between the Arrange, Act and Assert sections so each phase is visually
separated: e.g. separate the setup (mocks.activeDomains = undefined; const {
result, onSent } = renderSend(...); expect(result.current.encryptionState)...),
the act (await act(async () => { await result.current.send(); });), and the
assertions (expect(show)... expect(mocks.sendEmail)... expect(onSent)...). Apply
the same blank-line separation to the other tests referenced (lines 101-112,
127-142, 144-171, 189-222, 224-241) so Arrange/Act/Assert blocks are clearly
separated around calls to renderSend, act, send, and subsequent expect
assertions.

In `@src/services/network/index.ts`:
- Around line 40-53: The attachmentsSessionKey parameter is currently typed as
required Uint8Array but the code conditionally checks it, so change the method
signature/destructured param type to attachmentsSessionKey?: Uint8Array (or
Uint8Array | undefined) and update any callers to pass undefined when no key is
available; keep the guard if (attachmentsSessionKey) before calling
decryptSymmetrically so decryption only runs when a key is provided, and ensure
payload is constructed from either the raw data or the decrypted result (use new
Uint8Array(data) for the non-encrypted path and the decrypted Uint8Array for the
encrypted path) while preserving the returned Blob creation and types.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5a6249b5-1075-4867-b3eb-6d6c0b182af0

📥 Commits

Reviewing files that changed from the base of the PR and between 86f1a91 and 84b9449.

📒 Files selected for processing (7)
  • .env.example
  • src/components/compose-message/hooks/useComposeSend.test.ts
  • src/components/compose-message/hooks/useComposeSend.ts
  • src/components/compose-message/index.tsx
  • src/services/config/index.ts
  • src/services/network/index.ts
  • src/types/config/index.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant