@aglyn/shared-util-email
v1.0.0-beta.180
Published
> Beta. Published from the Aglyn monorepo under the `beta` dist-tag; APIs can change between beta releases.
Readme
@aglyn/shared-util-email
Beta. Published from the Aglyn monorepo under the
betadist-tag; APIs can change between beta releases.
Install
npm install @aglyn/shared-util-email@betaOne place that sends outbound application email, via Resend.
Before this library the same ~30 lines of fetch('https://api.resend.com/emails')
were copy-pasted across 10 files, each reading the env vars itself and each
handling failure a little differently. Consolidating them means the provider,
the sender identity, and the failure semantics change in one place.
What is and is not in scope
| Mail | Sent by | Here? |
| --- | --- | --- |
| Invites, receipts, usage summaries, campaigns, staff alerts | Resend | ✅ |
| Verification, password reset | Firebase Auth | ❌ |
| Inbound mail to @aglyn.com mailboxes | Google Workspace | ❌ |
Configuration
Two env vars, read at call time:
RESEND_API_KEY=re_xxxxxxxx
USAGE_EMAIL_FROM="Aglyn <[email protected]>"Both are required. With either missing, sendEmail() warns and returns
{ sent: false, reason: 'unconfigured' } — it does not throw, so local and
preview environments run fine without a Resend account.
Setup and DNS are documented in docs/EMAIL_SETUP.md.
Usage
import { sendEmail } from '@aglyn/shared-util-email'
const result = await sendEmail({
to: '[email protected]',
subject: 'You have been invited',
text: 'Sign in to accept.',
context: 'invite', // shows up in logs on failure
})
if (!result.sent) {
// 'unconfigured' | 'no-recipient' | 'rejected' | 'network'
console.warn('no mail went out:', result.reason)
}sendEmail() never throws. Outbound mail is best-effort everywhere in
this codebase — a checkout must not fail because a receipt bounced — so every
outcome comes back as a result object. Do check sent: it is what lets the
console tell a user honestly whether a message actually went out.
Optional fields: html, headers (e.g. List-Unsubscribe), tags (webhook
attribution), replyTo, and from (overrides the configured sender — rarely
correct, since the point of USAGE_EMAIL_FROM is one verified identity).
The transport boundary
postResendEmail() is the only function that POSTs to Resend's send endpoint,
and it throws on a payload with no to rather than putting it on the wire.
Such a payload cannot become a message; Resend answers 422
missing_required_field, which costs an API call and then sits in the vendor
dashboard looking exactly like mail that failed to deliver, carrying nothing
that names the code responsible. sendEmail() filters recipients long before
this point, so ordinary senders never meet the guard — it is there because
RESEND_SEND_ENDPOINT is exported and a module that fetches it directly
bypasses every check sendEmail() owns.
Checking configuration
import { isEmailConfigured, describeEmailConfig, checkEmailCredentials }
from '@aglyn/shared-util-email'isEmailConfigured()— both vars present. Use it to answer501from a route instead of pretending to have sent.describeEmailConfig()— env presence plus the sender and its domain, with the API key never included.checkEmailCredentials()— asks Resend whether the key is accepted without sending anything, byGETting the domains collection and reading the error NAME rather than the status. A2xx, or arestricted_api_key/invalid_permissionrejection (what a sending-scoped key gets, and it can only be reached once Resend has authenticated the key), means the credential works.missing_api_key,validation_errorandsuspended_api_keymean it was refused. Anything else isunknown, neverinvalid-key. It never touches the send endpoint: a probe aimed there is logged by Resend as a422onPOST /emailsand reads, in the dashboard, as failed mail. It cannot confirm domain verification — only a real send does that.
These back the staff-only /api/admin/email-health route in the console.
Running unit tests
Run nx test shared-util-email to execute the unit tests via
Jest.
Note: prefer bare jest over nx test when a test depends on env vars — nx
injects the root .env, which can turn a genuinely failing test green.
License
Apache-2.0. Source: https://github.com/aglyn/aglyn/tree/main/libs/shared/util/email