Skip to content

Feat/gmail unsubscribe agent#1657

Merged
senamakel merged 10 commits into
tinyhumansai:mainfrom
HereIsKrishna:feat/gmail-unsubscribe-agent
May 16, 2026
Merged

Feat/gmail unsubscribe agent#1657
senamakel merged 10 commits into
tinyhumansai:mainfrom
HereIsKrishna:feat/gmail-unsubscribe-agent

Conversation

@HereIsKrishna
Copy link
Copy Markdown
Contributor

@HereIsKrishna HereIsKrishna commented May 13, 2026

Summary

  • Extracted List-Unsubscribe headers from raw Gmail payloads within the Composio ingest pipeline.
  • Surfaced list_unsubscribe metadata to the canonicalized EmailMessage struct and the agent's markdown render tree.
  • Authored the new GmailUnsubscribeTool to securely intercept agent unsubscribe intents.
  • Built the UnsubscribeApprovalCard React component to manage user-approved tool executions in the chat UI.

Problem

  • The platform lacked a native way to bulk-unsubscribe from newsletters and promotional emails.
  • Allowing an autonomous agent to silently execute arbitrary mailto: or HTTPS unsubscribe links from email bodies introduces a critical security risk (accidental unsubscriptions or malicious link execution).

Solution

  • Reliably extracts the native RFC 2369 List-Unsubscribe headers directly during email ingestion, avoiding fragile regex parsing on the email body.
  • The new gmail_unsubscribe tool utilizes the PendingAction pattern, immediately halting execution and returning a pending_approval state.
  • A human-in-the-loop barrier was established via the new UnsubscribeApprovalCard UI, guaranteeing that the agent cannot proceed with network requests until the user explicitly clicks "Approve".

Submission Checklist

If a section does not apply to this change, mark the item as N/A with a one-line reason. Do not delete items.

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy - N/A: Behavior-only addition; relies on existing tool registry tests.
  • Diff coverage ≥ 80%N/A: Offloaded to CI diff-cover gating.
  • Coverage matrix updated — N/A: behaviour-only change
  • All affected feature IDs from the matrix are listed in the PR description under ## Related
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • Manual smoke checklist updated if this touches release-cut surfaces - N/A
  • Linked issue closed via Closes #NNN in the ## Related section

Impact

  • Runtime/Platform: Desktop & Web.
  • Security: Positively impacts security by enforcing a strict user-approval loop before executing third-party unsubscribe endpoints extracted from external emails.

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: feat/gmail-unsubscribe-agent
  • Commit SHA: HEAD

Validation Run

  • pnpm --filter openhuman-app format:check
  • pnpm typecheck
  • Focused tests: N/A
  • Rust fmt/check (if changed): Blocked locally
  • Tauri fmt/check (if changed): N/A

Validation Blocked

  • command: cargo check
  • error: Unable to find libclang: couldn't find any valid shared libraries matching clang.dll
  • impact: Missing local LLVM/ninja build dependencies for whisper-rs-sys on Windows environment. Offloading Rust checks and compilation to GitHub Actions CI pipeline.

Behavior Changes

  • Intended behavior change: Agents can now ingest List-Unsubscribe headers and prompt users to unsubscribe.
  • User-visible effect: A sleek "Unsubscribe Request" card appears in the chat feed awaiting Approval/Denial when the agent attempts to execute an unsubscribe action.

Parity Contract

  • Legacy behavior preserved: Yes, standard email canonicalization remains unaffected for emails without unsubscribe headers.
  • Guard/fallback/dispatch parity checks: Safe pending_approval guardrail actively enforced before remote execution.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): N/A
  • Canonical PR: N/A
  • Resolution (closed/superseded/updated): N/A

Summary by CodeRabbit

  • New Features

    • UI: Unsubscribe approval card to approve/deny unsubscribe requests, show processing state, errors, and confirmations.
    • Backend: Mail unsubscribe tool that emits pending-approval unsubscribe requests with sender and unsubscribe-link metadata.
    • Messages: Message formatting now includes a List-Unsubscribe header when present.
  • Tests

    • Added tests covering UI interactions and backend tool behavior, including approval, denial, error handling, and validation.

Review Change Stack

@HereIsKrishna HereIsKrishna requested a review from a team May 13, 2026 15:14
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c7a30dec-bb10-4f63-bc25-31cab7ccdeb8

📥 Commits

Reviewing files that changed from the base of the PR and between 71cb966 and 75cb812.

📒 Files selected for processing (1)
  • app/src/components/chat/__tests__/UnsubscribeApprovalCard.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/components/chat/tests/UnsubscribeApprovalCard.test.tsx

📝 Walkthrough

Walkthrough

Extracts List-Unsubscribe into EmailMessage, adds GmailUnsubscribeTool that returns a pending-approval unsubscribe action, registers the tool, and adds a frontend UnsubscribeApprovalCard that asks for user approval and calls core RPC to execute the unsubscribe.

Changes

Gmail Unsubscribe Workflow

Layer / File(s) Summary
Email data model enrichment
src/openhuman/memory/tree/canonicalize/email.rs, src/openhuman/composio/providers/gmail/ingest.rs, src/openhuman/composio/providers/gmail/post_process.rs
EmailMessage gains optional list_unsubscribe; ingestion/post-process extract List-Unsubscribe; canonicalizer conditionally renders the header; tests set the new field to None.
Backend unsubscribe tool
src/openhuman/tools/impl/network/gmail_unsubscribe.rs
Adds GmailUnsubscribeTool implementing Tool: requires sender and unsubscribe_link, validates link presence, logs a redacted sender, and returns a pending_approval JSON payload with unsubscribe action and metadata; includes unit tests for success and validation errors.
Tool registration
src/openhuman/tools/impl/network/mod.rs, src/openhuman/tools/ops.rs
Introduces gmail_unsubscribe module, re-exports GmailUnsubscribeTool, and registers it in all_tools_with_runtime.
Frontend approval UI
app/src/components/chat/UnsubscribeApprovalCard.tsx, app/src/components/chat/__tests__/UnsubscribeApprovalCard.test.tsx
Adds UnsubscribeApprovalCard component that shows pending approval requests for unsubscribe actions, manages pending/approved/denied state, invokes callCoreRpc('tools::execute_unsubscribe') with the unsubscribe_link on approve, and updates UI while disabling buttons during processing; tests cover render, approve, deny, and error handling.
sequenceDiagram
  participant User
  participant Frontend as UnsubscribeApprovalCard
  participant RPC as callCoreRpc
  participant Backend as GmailUnsubscribeTool
  User->>Frontend: Clicks "Approve & Unsubscribe"
  Frontend->>RPC: callCoreRpc('tools::execute_unsubscribe', {link})
  RPC->>Backend: execute_unsubscribe request
  Backend-->>RPC: pending_approval / error response
  RPC-->>Frontend: reply
  Frontend-->>User: show approved or revert to pending on error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I found a stray List-Unsubscribe string,

I hopped and nudged to make it sing,
A card appears — approve or deny,
Click approve, the newsletters fly,
I twitched my nose and waved goodbye.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat/gmail unsubscribe agent' clearly and concisely identifies the main feature being added—a Gmail unsubscribe agent capability.
Linked Issues check ✅ Passed The PR comprehensively implements all key acceptance criteria from issue #1534: unsubscribe intent recognition, safe header-based mechanisms (List-Unsubscribe), explicit user approval UI (UnsubscribeApprovalCard), proper error handling for missing permissions, and test coverage for the approval workflow.
Out of Scope Changes check ✅ Passed All changes directly support the Gmail unsubscribe feature: backend tool (GmailUnsubscribeTool), email data pipeline (extracting List-Unsubscribe header), memory canonicalization, and frontend approval UI. No unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

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

🧹 Nitpick comments (1)
app/src/components/chat/UnsubscribeApprovalCard.tsx (1)

17-18: ⚡ Quick win

Local status can become stale across payload changes.

status is initialized once and not synchronized to a new pending approval payload. If this component is reused (not remounted), a prior approved/denied state can suppress actions for the next request.

Please verify the parent render path keys/remounts this card per pending action. If not, reset state on payload identity change (e.g., action/request id key) or derive status from payload-driven state.

Also applies to: 43-43

🤖 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 `@app/src/components/chat/UnsubscribeApprovalCard.tsx` around lines 17 - 18,
The local `status` and `isProcessing` state in UnsubscribeApprovalCard (the
`status`, `setStatus`, `isProcessing`, `setIsProcessing` useState hooks) can
become stale when a new pending approval payload is rendered into the same
mounted component; add logic to synchronize/reset state on payload identity
changes (e.g., watch the approval/request id prop or the payload object) by
resetting `setStatus('pending')` (and `setIsProcessing(false)` if appropriate)
in a useEffect that depends on that id/identity, or alternatively derive
`status` from the incoming payload instead of local state so a prior
`approved`/`denied` value cannot suppress actions for the next request.
🤖 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.

Inline comments:
In `@app/src/components/chat/UnsubscribeApprovalCard.tsx`:
- Around line 20-29: handleApprove can be re-entered on rapid clicks; add a
short-circuit at the start of handleApprove to return immediately if
isProcessing is true (and optionally if status === 'approved') to prevent
dispatching duplicate RPCs, then proceed to setIsProcessing(true) and call
callCoreRpc as before — reference handleApprove, isProcessing, setIsProcessing,
callCoreRpc, and status to locate and implement the guard.

In `@src/openhuman/tools/impl/network/gmail_unsubscribe.rs`:
- Around line 38-65: Add verbose, grep-friendly diagnostic logging to the async
fn execute to improve auditability: log entry at the start of execute (use
tracing::debug or log::debug with a stable prefix like
"GMAIL_UNSUBSCRIBE:ENTRY") including only a redacted sender (e.g., replace
local-part with "***" or show domain only), log the empty-link validation branch
before returning ToolResult::error with a prefix like
"GMAIL_UNSUBSCRIBE:VALIDATION:EMPTY_LINK", and log the pending-approval return
just before returning ToolResult::json with a prefix like
"GMAIL_UNSUBSCRIBE:PENDING_APPROVAL" and include only non-PII metadata (sender
redacted, unsubscribe_link omitted or safely hashed, and action/status fields)
so logs are grep-friendly and contain safe audit metadata; use tracing or log at
debug/trace level per project guidelines and keep message format consistent for
automated parsing.

---

Nitpick comments:
In `@app/src/components/chat/UnsubscribeApprovalCard.tsx`:
- Around line 17-18: The local `status` and `isProcessing` state in
UnsubscribeApprovalCard (the `status`, `setStatus`, `isProcessing`,
`setIsProcessing` useState hooks) can become stale when a new pending approval
payload is rendered into the same mounted component; add logic to
synchronize/reset state on payload identity changes (e.g., watch the
approval/request id prop or the payload object) by resetting
`setStatus('pending')` (and `setIsProcessing(false)` if appropriate) in a
useEffect that depends on that id/identity, or alternatively derive `status`
from the incoming payload instead of local state so a prior `approved`/`denied`
value cannot suppress actions for the next request.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6c336201-209d-460b-9414-717773569f90

📥 Commits

Reviewing files that changed from the base of the PR and between df189c8 and 6db406f.

📒 Files selected for processing (7)
  • app/src/components/chat/UnsubscribeApprovalCard.tsx
  • src/openhuman/composio/providers/gmail/ingest.rs
  • src/openhuman/composio/providers/gmail/post_process.rs
  • src/openhuman/memory/tree/canonicalize/email.rs
  • src/openhuman/tools/impl/network/gmail_unsubscribe.rs
  • src/openhuman/tools/impl/network/mod.rs
  • src/openhuman/tools/ops.rs

Comment thread app/src/components/chat/UnsubscribeApprovalCard.tsx
Comment thread src/openhuman/tools/impl/network/gmail_unsubscribe.rs
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 13, 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: 1

🧹 Nitpick comments (1)
app/src/components/chat/__tests__/UnsubscribeApprovalCard.test.tsx (1)

8-9: ⚡ Quick win

Add mock cleanup for test isolation.

The callCoreRpc mock is defined at module level and shared across all tests. While mockResolvedValueOnce and mockRejectedValueOnce isolate behavior per call, the mock's call history accumulates across tests, creating hidden global state that could affect test determinism.

🧹 Add beforeEach to reset mocks
 vi.mock('../../../services/coreRpcClient', () => ({ callCoreRpc: vi.fn() }));
 
 describe('UnsubscribeApprovalCard', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
   const mockPayload = {

As per coding guidelines, keep unit tests deterministic and avoid hidden global state.

🤖 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 `@app/src/components/chat/__tests__/UnsubscribeApprovalCard.test.tsx` around
lines 8 - 9, Add a beforeEach hook to reset the shared mock state so call
histories don't leak between tests: the module-level vi.mock of
../../../services/coreRpcClient that exposes callCoreRpc (a vi.fn()) accumulates
call history, so add a beforeEach(() => vi.clearAllMocks() or
vi.resetAllMocks()) in UnsubscribeApprovalCard.test.tsx to clear
calls/implementations before each test, ensuring test isolation for mocks like
callCoreRpc.
🤖 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.

Inline comments:
In `@app/src/components/chat/__tests__/UnsubscribeApprovalCard.test.tsx`:
- Around line 28-32: Add a new unit test in UnsubscribeApprovalCard.test.tsx
that mirrors the existing "action" guard test but verifies the status guard:
create a payload by spreading mockPayload and setting status to 'completed' (or
any value other than 'pending_approval'), render <UnsubscribeApprovalCard
payload={payload} /> using renderWithProviders, and assert that container is an
empty DOM element; follow the same test structure and naming convention as the
existing "returns null if action is not unsubscribe" test to ensure both guards
(action and status) are covered.

---

Nitpick comments:
In `@app/src/components/chat/__tests__/UnsubscribeApprovalCard.test.tsx`:
- Around line 8-9: Add a beforeEach hook to reset the shared mock state so call
histories don't leak between tests: the module-level vi.mock of
../../../services/coreRpcClient that exposes callCoreRpc (a vi.fn()) accumulates
call history, so add a beforeEach(() => vi.clearAllMocks() or
vi.resetAllMocks()) in UnsubscribeApprovalCard.test.tsx to clear
calls/implementations before each test, ensuring test isolation for mocks like
callCoreRpc.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0ba1ce7b-01db-4b6b-9508-876c664c3637

📥 Commits

Reviewing files that changed from the base of the PR and between 49254d0 and 1881025.

📒 Files selected for processing (3)
  • app/src/components/chat/UnsubscribeApprovalCard.tsx
  • app/src/components/chat/__tests__/UnsubscribeApprovalCard.test.tsx
  • src/openhuman/tools/impl/network/gmail_unsubscribe.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/openhuman/tools/impl/network/gmail_unsubscribe.rs
  • app/src/components/chat/UnsubscribeApprovalCard.tsx

Comment thread app/src/components/chat/__tests__/UnsubscribeApprovalCard.test.tsx
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 13, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 13, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 13, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 16, 2026
@senamakel senamakel self-assigned this May 16, 2026
…bit nits

- Add missing `list_unsubscribe: None` to EmailMessage literals in
  `tests/agent_retrieval_e2e.rs` and the `pick_thread_subject_*` unit
  tests in `composio/providers/gmail/ingest.rs` so the workspace tests
  compile against the new field added to `EmailMessage`.
- UnsubscribeApprovalCard.test.tsx: clear shared `callCoreRpc` mock state
  between tests (`beforeEach(vi.clearAllMocks)`) and add a guard test
  asserting the card renders null when `status !== 'pending_approval'`.
@senamakel senamakel merged commit 30345cc into tinyhumansai:main May 16, 2026
20 of 21 checks passed
@HereIsKrishna HereIsKrishna deleted the feat/gmail-unsubscribe-agent branch May 16, 2026 13:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Let users unsubscribe from Gmail emails through the agent

2 participants