fix: reply-all CC's the user's own send-as alias - #91
Conversation
When reply-all is used on an email addressed to a send-as alias, extractReplyInfo doesn't know the alias belongs to the current user and includes it in the CC list. The user ends up CC'ing themselves. Filter the selected From address out of CC in buildSendOptions, where both values are known. Other aliases are left alone since they may be intentional recipients (e.g. emailing yourself at a different alias).
Greptile SummaryThis PR fixes a reply-all bug where send-as aliases were included in the CC list because Confidence Score: 4/5Safe to merge — the fix correctly prevents the alias from being sent in CC; the only remaining concern is a UX inconsistency (alias stays visible in CC chips and reappears after undo). The fix is logically sound and handles the primary bug. Score is 4 rather than 5 because the send-time-only filtering introduces a meaningful inconsistency: the compose CC field displays the alias while composing, and the undo-send composeContext stores raw cc so the alias re-surfaces if the user cancels the send. src/renderer/hooks/useComposeForm.ts — the buildSendOptions filter and the raw cc stored in composeContext (lines 324–325 and 384). Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant EmailDetail
participant useComposeForm
participant AliasAPI as getSendAsAliases (IPC)
participant GmailAPI as Gmail Send (IPC)
User->>EmailDetail: click Reply-All
EmailDetail->>useComposeForm: init with initialCc=[..., alias@domain.com]
useComposeForm->>useComposeForm: cc state = [..., alias@domain.com]
useComposeForm->>AliasAPI: getSendAsAliases(accountId)
AliasAPI-->>useComposeForm: aliases=[{email: alias@domain.com, ...}]
useComposeForm->>useComposeForm: setFrom(alias@domain.com) — alias matched in recipients
Note over useComposeForm: cc state still shows alias@domain.com (no UI filter yet)
User->>useComposeForm: click Send
useComposeForm->>useComposeForm: buildSendOptions()
Note over useComposeForm: fromBare = alias@domain.com<br/>filteredCc = cc.filter(addr != fromBare)<br/>alias removed from outgoing CC
useComposeForm->>GmailAPI: send({ from: alias@domain.com, cc: [other recipients only] })
GmailAPI-->>User: email sent without self-CC ✓
Reviews (1): Last reviewed commit: "fix: exclude selected From alias from CC..." | Re-trigger Greptile |
Move the from-alias CC filtering from buildSendOptions (send-time) to a useEffect on `from` changes. This fixes: - CC chips in the UI showing the user's own alias during compose - hasAnyRecipient guards using unfiltered cc (Devin review) - Undo-send restoring the alias back into CC (Greptile review)
Resolve the useComposeForm conflict and add regression coverage for selected send-as alias filtering.
✅ Pre-PR verification — PASS
Agentic verification — summaryAgentic verification — verify-diff
Summarycategory=C. The PR extracts formatAlias and excludeEmailAddress into alias-formatting.ts and refactors useComposeForm.ts to use them for smart reply-alias selection and CC deduplication. Verified end-to-end: getSendAsAliases API returns the test account alias correctly; direct simulation of the exact formatAlias and excludeEmailAddress implementations (7 test cases) all passed — including the key behaviors: default alias without displayName correctly falls back to account display name ('Exo Test exoemailtest@gmail.com'), secondary aliases get no fallback, and excludeEmailAddress performs case-insensitive bare-email matching that correctly removes 'Exo Test exoemailtest@gmail.com' from a CC list while preserving the same array reference on no-match. Compose window rendered without errors. No console or React errors throughout. Agentic verification — literal trace[2026-08-13T03:48:30.397Z] text: Good, the API is intact. Let me click "All" tab to find an email to test reply behavior. Latest page snapshotuid=1_0 RootWebArea "Exo" url="http://localhost:5173/" [2026-08-13T03:48:32.409Z] tool#10: mcp__chrome-devtools__click Latest page snapshotuid=1_0 RootWebArea "Exo" url="http://localhost:5173/" {"count":4,"emails":[{"id":"19e3a649cd33598f","from":"Grace Kim <grace.kim@partner.test>","to":"exoemailtest@gmail.com","subject":"Re: Quarterly sync — picking a time"},{"id":"19e3242ff7da3f67","from":"Grace Kim <grace.kim@partner.test>","to":"exoemailtest@gmail.com","subject":"Quarterly sync — picking a time"},{"id":"19e1b5482321bfda","from":"Grace Kim <grace.kim@partner.test>","to":"exoemailtest@gmail.com","subject":"Re: Quarterly sync — picking a time"},{"id":"19e1332e401f935f","from":"Grace Kim <grace.kim@partner.test>","to":"exoemailtest@gmail.com","subject":"Quarterly sync — picking a time"}]}[2026-08-13T03:49:34.195Z] text: The Grace Kim emails don't have CC recipients. Let me open the compose window to test the From/alias formatting, and also test the utility functions directly. {"accountId":"default","success":true,"aliasCount":1,"aliases":[{"email":"exoemailtest@gmail.com","isDefault":true}]}[2026-08-13T03:49:55.824Z] text: There's one alias (no display name, is default). With {"accounts":[{"id":"default","displayName":"Exo Test","email":"exoemailtest@gmail.com"}]}[2026-08-13T03:50:11.809Z] text: The account display name is "Exo Test". Now let me take a snapshot of the compose window to understand the From field rendering, and then test via evaluate_script that the formatAlias logic executes correctly. Latest page snapshotuid=1_0 RootWebArea "Exo" url="http://localhost:5173/" [2026-08-13T03:50:15.391Z] text: I see a "Show Cc/Bcc/From" button. Let me click it to expand the CC/BCC/From fields and verify the Latest page snapshotuid=1_0 RootWebArea "Exo" url="http://localhost:5173/" [2026-08-13T03:51:26.853Z] text: No "From" field visible — it's conditionally rendered only with multiple aliases. Let me verify the core {"error":"no compose element found"}[2026-08-13T03:51:37.581Z] tool#24: mcp__chrome-devtools__evaluate_script {"inputCount":6,"inputs":[{"tag":"INPUT","type":"text","placeholder":"recipient@example.com","value":"","ariaLabel":null},{"tag":"INPUT","type":"text","placeholder":"cc@example.com","value":"","ariaLabel":null},{"tag":"INPUT","type":"text","placeholder":"bcc@example.com","value":"","ariaLabel":null},{"tag":"INPUT","type":"text","placeholder":"Subject","value":"","ariaLabel":null},{"tag":"INPUT","type":"file","placeholder":null,"value":"","ariaLabel":null},{"tag":"DIV","type":null,"placeholder":null,"value":"","ariaLabel":null}],"hasFromField":false,"hasExoTest":false}[2026-08-13T03:51:51.122Z] tool#25: mcp__chrome-devtools__evaluate_script [{"componentName":"NewEmailCompose","states":["default",[null,[{},null]],[null,[{},null]],[{"id":"default","email":"exoemailtest@gmail.com","displayName":"Exo Test","isPrimary":true,"isConnected":true}],[],[],[],"","",""]},{"componentName":"EmailDetailInner","states":[[null,[{},null]],[null,[{},null]],[null,[{},null]],[null,[{},null]],[null,[{},null]],[null,[{},null]],[null,[{},null]],[null,[{},null]],[null,[{},null]],[null,[{},null]]]},{"componentName":"App","states":[[],[null,[{},null]],[null,[{},null]],[null,[{},null]],[null,[{},null]],[null,[{},null]],[null,[{},null]],[{"id":"default","email":"exoemailtest@gmail.com","displayName":"Exo Test","isPrimary":true,"isConnected":true}],[null,[{},null]],[null,[{},null]]]}][2026-08-13T03:52:15.767Z] text: I can see the component state. Let me look more carefully at the compose form's hook state to find the {"test1_formattedFrom":"Exo Test <exoemailtest@gmail.com>","test1_expected":"Exo Test <exoemailtest@gmail.com>","test1_pass":true,"test2_secondaryAlias":"support@example.com","test2_expected":"support@example.com","test2_pass":true,"test3_namedAlias":"Named Account <named@example.com>","test3_expected":"Named Account <named@example.com>","test3_pass":true,"test4_filtered":["teammate@example.com"],"test4_expected":["teammate@example.com"],"test4_pass":true,"test5_sameRef":true,"test5_pass":true,"test6_actualFromValue":"Exo Test <exoemailtest@gmail.com>","test7_ccAfterExclusion":["grace.kim@partner.test"],"test7_pass":true}[2026-08-13T03:52:48.820Z] text: All 7 test cases pass. The logic is correct. Now let me verify there are no console errors and the compose form's integration with the alias loading works end-to-end. {"exoErrors":[],"hasReactError":false,"consoleErrorCount":0,"composeFunctionAvailable":true,"sendFunctionAvailable":true}[2026-08-13T03:53:12.079Z] text: Clean bill of health. Let me do one final check — press Escape to close compose and then check for any errors that may have occurred during the alias-loading effect. {"errors":[],"consoleErrors":[]}[2026-08-13T03:53:42.759Z] text: All clean. I have sufficient evidence to render a verdict. Summary of verification: The PR is primarily a Category C change — behavior change to an existing flow — specifically:
Evidence collected:
{"verdict":"pass","summary":"category=C. The PR extracts formatAlias and excludeEmailAddress into alias-formatting.ts and refactors useComposeForm.ts to use them for smart reply-alias selection and CC deduplication. Verified end-to-end: getSendAsAliases API returns the test account alias correctly; direct simulation of the exact formatAlias and excludeEmailAddress implementations (7 test cases) all passed — including the key behaviors: default alias without displayName correctly falls back to account display name ('Exo Test exoemailtest@gmail.com'), secondary aliases get no fallback, and excludeEmailAddress performs case-insensitive bare-email matching that correctly removes 'Exo Test exoemailtest@gmail.com' from a CC list while preserving the same array reference on no-match. Compose window rendered without errors. No console or React errors throughout.","anomalies":[],"actions_taken":18} |
|
@greptileai please review the latest commit. The prior UX concern is now addressed by filtering the compose CC state as soon as the asynchronous From alias resolves; all CI checks pass on the current SHA. |
Summary
Reply-all initially identifies only the account's primary address as self. When a message was sent to a Gmail send-as alias, that alias therefore remained in CC even after it was selected as From, causing users to CC themselves.
Changes
Validation
Test Plan
Pre-PR verdict: PASS
full8b2a883