stack/packages/shared/src
BilalG1 81433e5181
feat(emails): arbitrary-recipient send + Emailable on managed domains (#1681)
## Summary

Two related email changes:

### 1. Send to arbitrary email addresses
The `sendEmail` SDK function and `POST /api/v1/emails/send-email` now
accept a third recipient selector, **`emails: string[]`**, mutually
exclusive (XOR) with `user_ids` and `all_users`. These recipients don't
have to belong to a user — they're mapped to the existing internal
`custom-emails` recipient type (no associated user object, can't
unsubscribe, so intended for transactional mail).

- Response `results` items are now `{ user_id?, email? }`; the `emails`
path returns `{ email }`.
- Validation message updated to `Exactly one of user_ids, all_users, or
emails must be provided`.
- Recipient addresses are validated with the shared `emailSchema`.

### 2. Run Emailable on managed custom domains
`LowLevelEmailConfig.type` is widened `'shared' | 'standard'` →
`'shared' | 'managed' | 'standard'`, and `getEmailConfig` returns
`'managed'` for the managed-Resend branch. The Emailable deliverability
check now runs for **shared** and **managed**, and is skipped for
**`standard`** (a customer's own SMTP server or Resend API key).

**Why:** managed domains send through our single Resend *account*
(per-domain scoped keys, but shared account → account-level bounce
penalties are collateral across managed customers), so we own that
reputation. With a custom SMTP server or the customer's own Resend key,
the customer owns their deliverability, so we don't second-guess their
recipients or spend Emailable checks on their volume. The shared
dev-email wrapper remains `shared`-only.

## Tests
- `send-email.test.ts`: added arbitrary-recipient send
(single/multiple), empty-array, and validation cases (both-selectors,
no-selector, invalid email); updated the exactly-one-of snapshot.
**24/24 pass.**
- New `emails/deliverability-gating.test.ts`: shared & managed skip an
undeliverable address (`LIKELY_NOT_DELIVERABLE`), custom SMTP still
sends. **3/3 pass.**

> Note: `email-queue.test.ts` has 2 pre-existing snapshot failures (a
`margin:0rem`→`margin:0` CSS normalization in rendered email HTML) that
are unrelated to this change — confirmed by reproducing them on `dev`
with these changes stashed.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds support for sending emails to arbitrary addresses and enables
Emailable checks for managed domains to protect shared sending
reputation. The API/SDK now accept `emails: string[]` as a recipient
selector, and deliverability checks run on shared and managed sending.

- New Features
- API/SDK: `emails: string[]` recipient selector (XOR with
`user_ids`/`all_users`) in `sendEmail` and POST
/api/v1/emails/send-email. Results return `{ email }` for this path;
addresses validated via `emailSchema`. Recipients map to
transactional-only “custom-emails” (no user, no unsubscribe).
- Deliverability: email config adds `'managed'`; `getEmailConfig`
returns it for managed Resend domains. Emailable check runs for `shared`
and `managed`; skipped for `standard` (custom SMTP or customer Resend
key).

<sup>Written for commit e48cf2ddda.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1681?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Emails can now be sent to arbitrary email addresses, not just existing
users.
* Email sending APIs and client options now accept an email list as a
recipient choice.

* **Bug Fixes**
* Improved recipient validation so exactly one target type must be
chosen.
* Fixed response results to consistently show either a user ID or an
email address.
* Corrected deliverability checks for managed email setups and updated
skip behavior for undeliverable addresses.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-30 11:16:25 -07:00
..
ai Remove local emulator support (#1692) 2026-06-30 10:27:01 -07:00
apps Make it clear there are more SDK packages 2026-06-16 10:37:58 -07:00
config refactor: migrate config parsing from Babel AST to jiti (#1661) 2026-06-29 10:25:11 -07:00
helpers stack dev -> hexclave dev 2026-06-26 16:05:34 -07:00
hooks feat(hexclave): PR 5 — internal symbol/path/package renames + brand strings (#1547) 2026-06-03 18:57:09 -07:00
interface feat(emails): arbitrary-recipient send + Emailable on managed domains (#1681) 2026-06-30 11:16:25 -07:00
payments feat(hexclave): PR 5 — internal symbol/path/package renames + brand strings (#1547) 2026-06-03 18:57:09 -07:00
utils Usage page performance improvements (#1650) 2026-06-24 12:25:20 -07:00
config-authoring.test.ts Replace writeConfigObject with AI-aware updateConfigObject (#1537) 2026-06-15 12:00:24 -07:00
config-authoring.ts Stop using app.urls everywhere 2026-06-23 11:22:44 -07:00
config-eval.ts refactor: migrate config parsing from Babel AST to jiti (#1661) 2026-06-29 10:25:11 -07:00
config-rendering.ts refactor: migrate config parsing from Babel AST to jiti (#1661) 2026-06-29 10:25:11 -07:00
crud.tsx feat(hexclave): PR 5 — internal symbol/path/package renames + brand strings (#1547) 2026-06-03 18:57:09 -07:00
global.d.ts feat(hexclave): PR 5 — internal symbol/path/package renames + brand strings (#1547) 2026-06-03 18:57:09 -07:00
index.ts feat(hexclave): PR 5 — internal symbol/path/package renames + brand strings (#1547) 2026-06-03 18:57:09 -07:00
known-errors.tsx Better UX around hosted component errors 2026-06-04 17:27:04 -07:00
plans.ts feat(hexclave): PR 5 — internal symbol/path/package renames + brand strings (#1547) 2026-06-03 18:57:09 -07:00
schema-fields.ts refactor: migrate config parsing from Babel AST to jiti (#1661) 2026-06-29 10:25:11 -07:00
sessions.test.ts fix(rde): stop the RDE dashboard blanking on every access-token refresh (#1566) 2026-06-10 11:13:33 -07:00
sessions.ts fix(rde): stop the RDE dashboard blanking on every access-token refresh (#1566) 2026-06-10 11:13:33 -07:00