Skip to content

feat(email): Block sender frontend + backend improvements#2156

Merged
evanhutnik merged 6 commits intomainfrom
evan/block-user-fe
Mar 25, 2026
Merged

feat(email): Block sender frontend + backend improvements#2156
evanhutnik merged 6 commits intomainfrom
evan/block-user-fe

Conversation

@evanhutnik
Copy link
Copy Markdown
Contributor

Summary

  • Block sender UI: Adds "Block Sender" to the email thread command menu (Cmd+K), thread top bar (prohibit icon button), and soup right-click context menu for email entities
  • Backend improvements to existing block endpoints (from feat(email-service): Block users #1114):
    • Returns 403 instead of 500 when Gmail returns a Forbidden error due to missing OAuth scope (gmail.settings.basic)
    • Changes DELETE /contacts/block/{email_address} to POST /contacts/unblock with JSON body to avoid URL encoding issues with special chars in email addresses
    • Adds 403 to OpenAPI response specs for all three block/unblock/list-blocked endpoints
    • Adds GmailError::Forbidden variant to the Gmail client error enum
  • Frontend error handling: Adds FORBIDDEN error code to safeFetch, shows a permissions toast prompting re-authentication on 403
  • Shared logic: blockSenderWithToast() utility in queries layer handles block API call, success/error toasts with undo support -- used by both thread view and soup context menu
  • Sender detection: Skips current user's messages when determining who to block from a thread

Details

Block sender flow

  1. User triggers block from command menu, top bar button, or soup context menu
  2. Frontend resolves the sender email (first non-self message in thread, or senderEmail from entity)
  3. POST /email/contacts/block creates a Gmail filter that sends the sender's emails to trash
  4. Success toast shows "Sender blocked" with the email and an "Undo" button
  5. Undo calls POST /email/contacts/unblock to remove the filter

Permission handling

  • If the user lacks the gmail.settings.basic OAuth scope, Gmail returns 403
  • Backend now maps this to a 403 response with a clear message
  • Frontend shows a 5-second toast: "Insufficient permissions -- Enable new email permissions on sign-in"

@evanhutnik evanhutnik requested review from a team as code owners March 24, 2026 20:56
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 24, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6fa284f7-f3ca-4490-89bb-2da62540418b

📥 Commits

Reviewing files that changed from the base of the PR and between a7402b4 and 31f0de3.

📒 Files selected for processing (2)
  • js/app/packages/block-email/component/Email.tsx
  • js/app/packages/block-email/component/TopBar.tsx

Walkthrough

This PR implements sender-blocking end-to-end: adds a Block Sender UI action (soup menu item, toolbar tool, hotkey, and EmailContext.blockSender), a makeBlockSenderAction factory, and blockSenderWithToast helper. Service client adds emailClient.blockSender and emailClient.unblockSender. OpenAPI and server handlers change: unblock moved to POST /email/contacts/unblock with JSON body, GET /email/contacts/blocked updated, and 403 Forbidden responses are surfaced for permission failures. Error types and fetch handling were extended to include FORBIDDEN.

Suggested reviewers

  • whutchinson98

Poem

🐇 I hopped through code and found a tasty feat,
A button, hotkey, toast—now spam takes a seat.
From Inbox to server the blocker danced through,
I thump my foot and whisper, “That sender, adieu.” 🚫🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(email): Block sender frontend + backend improvements' clearly and specifically summarizes the main changes—adding a block sender feature to the frontend and improving the backend endpoints.
Description check ✅ Passed The description is directly related to the changeset, providing detailed context about the block sender UI additions, backend improvements, error handling, and implementation flow.

✏️ 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 evan/block-user-fe

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

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@js/app/packages/app/component/next-soup/actions/make-block-sender-action.ts`:
- Around line 10-15: The execute function currently loops entities and calls
blockSenderWithToast for every entity, causing duplicate attempts when multiple
entities share the same senderEmail; change execute to first collect unique
senderEmail values (e.g., using a Set while preserving order) from the entities
array (only for entity.type === 'email' and entity.senderEmail present) and then
iterate over that deduplicated list to await blockSenderWithToast for each
unique address; reference execute, entities, senderEmail, and
blockSenderWithToast when making the change.

In `@js/app/packages/block-email/component/EmailContext.tsx`:
- Around line 363-366: The current sender selection uses currentUserEmail()
without guarding for undefined, allowing your own address to be considered
blockable; update the logic around userEmail and senderEmail (the userEmail =
currentUserEmail() call and the thread.messages.find(...) that sets senderEmail)
to first guard for a missing currentUserEmail()—either return/skip selection
when currentUserEmail() is falsy or set userEmail to null and change the find
predicate to only compare toLowerCase() when userEmail is present (e.g.,
short-circuit the comparison with userEmail) so you never classify the user's
own address as blockable when currentUserEmail() is unavailable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d484e273-ffa5-48fe-b25d-1d5d0e10b20e

📥 Commits

Reviewing files that changed from the base of the PR and between f53e406 and e79024d.

⛔ Files ignored due to path filters (3)
  • js/app/packages/service-clients/service-email/generated/client.ts is excluded by !**/generated/**
  • js/app/packages/service-clients/service-email/generated/schemas/index.ts is excluded by !**/generated/**
  • js/app/packages/service-clients/service-email/generated/schemas/unblockSenderRequest.ts is excluded by !**/generated/**
📒 Files selected for processing (19)
  • js/app/packages/app/component/next-soup/actions/index.ts
  • js/app/packages/app/component/next-soup/actions/make-block-sender-action.ts
  • js/app/packages/app/component/next-soup/soup-view/soup-entity-actions-menu.tsx
  • js/app/packages/block-email/component/Email.tsx
  • js/app/packages/block-email/component/EmailContext.tsx
  • js/app/packages/block-email/component/TopBar.tsx
  • js/app/packages/block-email/util/emailHotkeys.ts
  • js/app/packages/core/hotkey/tokens.ts
  • js/app/packages/core/util/safeFetch.ts
  • js/app/packages/queries/email/thread.ts
  • js/app/packages/service-clients/service-email/client.ts
  • js/app/packages/service-clients/service-email/openapi.json
  • rust/cloud-storage/email_service/src/api/email/contacts/block_sender.rs
  • rust/cloud-storage/email_service/src/api/email/contacts/list_blocked.rs
  • rust/cloud-storage/email_service/src/api/email/contacts/mod.rs
  • rust/cloud-storage/email_service/src/api/email/contacts/unblock_sender.rs
  • rust/cloud-storage/email_service/src/api/swagger.rs
  • rust/cloud-storage/gmail_client/src/filters.rs
  • rust/cloud-storage/models_email/src/gmail/error.rs

Comment on lines +10 to +15
const execute = async (entities: EntityData[]) => {
for (const entity of entities) {
if (entity.type !== 'email' || !entity.senderEmail) continue;
await blockSenderWithToast(entity.senderEmail);
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Deduplicate sender emails to avoid redundant block attempts.

When multiple selected entities share the same senderEmail, the loop will attempt to block the same address multiple times, resulting in duplicate toasts and potential 409 conflict errors from the API.

🔧 Proposed fix
 const execute = async (entities: EntityData[]) => {
+  const seen = new Set<string>();
   for (const entity of entities) {
     if (entity.type !== 'email' || !entity.senderEmail) continue;
+    if (seen.has(entity.senderEmail)) continue;
+    seen.add(entity.senderEmail);
     await blockSenderWithToast(entity.senderEmail);
   }
 };
📝 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
const execute = async (entities: EntityData[]) => {
for (const entity of entities) {
if (entity.type !== 'email' || !entity.senderEmail) continue;
await blockSenderWithToast(entity.senderEmail);
}
};
const execute = async (entities: EntityData[]) => {
const seen = new Set<string>();
for (const entity of entities) {
if (entity.type !== 'email' || !entity.senderEmail) continue;
if (seen.has(entity.senderEmail)) continue;
seen.add(entity.senderEmail);
await blockSenderWithToast(entity.senderEmail);
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/app/packages/app/component/next-soup/actions/make-block-sender-action.ts`
around lines 10 - 15, The execute function currently loops entities and calls
blockSenderWithToast for every entity, causing duplicate attempts when multiple
entities share the same senderEmail; change execute to first collect unique
senderEmail values (e.g., using a Set while preserving order) from the entities
array (only for entity.type === 'email' and entity.senderEmail present) and then
iterate over that deduplicated list to await blockSenderWithToast for each
unique address; reference execute, entities, senderEmail, and
blockSenderWithToast when making the change.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
js/app/packages/block-email/component/EmailContext.tsx (1)

372-372: 🧹 Nitpick | 🔵 Trivial

Prefix with void to mark the intentional fire-and-forget call.

blockSenderWithToast is an async function returning a Promise. While errors are handled internally via toasts, the floating promise should be explicitly marked with void to communicate intent and satisfy linters that flag unhandled promises.

Suggested fix
-    blockSenderWithToast(senderEmail);
+    void blockSenderWithToast(senderEmail);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/app/packages/block-email/component/EmailContext.tsx` at line 372, The call
to the async function blockSenderWithToast should be explicitly marked as
fire-and-forget by prefixing it with void to avoid a floating promise; update
the invocation of blockSenderWithToast(senderEmail) inside the EmailContext
component (where the toast-handled async call is made) to use void
blockSenderWithToast(senderEmail) so linters and readers know the promise is
intentionally not awaited.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@js/app/packages/block-email/component/EmailContext.tsx`:
- Line 372: The call to the async function blockSenderWithToast should be
explicitly marked as fire-and-forget by prefixing it with void to avoid a
floating promise; update the invocation of blockSenderWithToast(senderEmail)
inside the EmailContext component (where the toast-handled async call is made)
to use void blockSenderWithToast(senderEmail) so linters and readers know the
promise is intentionally not awaited.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a80eb7c8-9823-4efd-9134-ec51852eb7d2

📥 Commits

Reviewing files that changed from the base of the PR and between e79024d and 9a6b1c0.

📒 Files selected for processing (1)
  • js/app/packages/block-email/component/EmailContext.tsx

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant