When "Send Email" is triggered from a People index with multiple rows
selected, the recipient list was built from the record store which only
contains fields loaded for the current view — emails.primaryEmail is
often missing if the email column is not visible.
In "select all" (exclusion) mode the problem was worse: selectedRecords
is empty so the composer always opened with a blank To: field.
Replace the record-store read with a dedicated useFindManyRecords query
using the context store's graphqlFilter, which correctly encodes both
inclusion and exclusion selections. The query fetches only id +
emails.primaryEmail and is capped at MAX_EMAIL_RECIPIENTS (100).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move email attachment uploads out of the workflow folder into a dedicated
EmailAttachment module with its own FileFolder, resolver, and service.
Add a 100-recipient cap (To+Cc+Bcc) enforced on both frontend and backend.
Extract shared AttachmentChip component for reuse across email and workflow.
Introduce a new "Open in app" click action for email fields that opens the
side-panel composer directly. This becomes the default for email fields.
When no connected account is available, it gracefully falls back to
mailto: instead of redirecting to settings.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
- **Inline email reply**: Replace external email client redirects
(Gmail/Outlook deeplinks) with an in-app email composer. Users can reply
to email threads directly from the email thread widget or via the
command menu.
- **SendEmail GraphQL mutation**: New backend mutation that reuses
`EmailComposerService` for body sanitization, recipient validation, and
SMTP dispatch via the existing outbound messaging infrastructure.
- **Side panel compose page**: Command menu "Reply" action now opens a
side-panel compose email page with pre-filled To, Subject, and
In-Reply-To fields.
### Backend
- `SendEmailResolver` with `SendEmailInput` / `SendEmailOutputDTO`
- `SendEmailModule` wired into `CoreEngineModule`
- Reuses `EmailComposerService` + `MessagingMessageOutboundService`
### Frontend
- `EmailComposer` / `EmailComposerFields` components
- `useSendEmail`, `useReplyContext`, `useEmailComposerState` hooks
- `useOpenComposeEmailInSidePanel` + `SidePanelComposeEmailPage`
- `EmailThreadWidget` inline Reply bar with toggle composer
- `ReplyToEmailThreadCommand` now opens side-panel instead of external
links
### Seeds
- Added `handle` field to message participant seeds for realistic email
addresses
- Seed `connectedAccount` and `messageChannel` in correct batch order
## Test plan
- [ ] Open an email thread on a person/company record → verify
"Reply..." bar appears below the last message
- [ ] Click "Reply..." → composer opens inline with pre-filled To and
Subject
- [ ] Type a message and click Send → email is sent via SMTP, composer
closes
- [ ] Use command menu Reply action → side panel opens with compose
email page
- [ ] Verify Send/Cancel buttons work correctly in side panel
- [ ] Test with Cc/Bcc toggle in composer fields
- [ ] Verify error handling: invalid recipients, missing connected
account
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>