--- title: "Send an email" description: "Configure Resend, send a test email through an operator task, and verify validation and delivery with a memory mailer." url: "https://www.beignetjs.com/guides/send-email" markdown_url: "https://www.beignetjs.com/markdown/guides/send-email.md" framework_version: "0.0.65" documentation_scope: "current-checkout" source_revision: "75f3e5c03f08daca9bba10940ebc777570febfd0" source_modified: true --- # Send an email Configure Resend, send a test message from an operator task, and test the same task with a memory mailer. Application code calls `ctx.ports.mailer`; the provider owns the connection to the email service. ## Before you start Use the Next.js and SQLite app from the [quickstart](https://www.beignetjs.com/markdown/getting-started.md). Run commands from its root with Bun. You can write and run the tests without an email account. Sending a real message requires a Resend API key and a sender on a [verified domain](https://resend.com/docs/dashboard/domains/introduction). This guide uses a [task](https://www.beignetjs.com/markdown/tasks.md) so you can send a message from your terminal without adding an HTTP endpoint. For mail triggered by a business operation, use a [job](https://www.beignetjs.com/markdown/jobs.md) and an [outbox](https://www.beignetjs.com/markdown/outbox.md) when delivery must survive a restart. ## Add the provider and task ```bash bun beignet provider add mail-resend bun beignet make task mail.send-test bun install ``` The provider preset adds the `mailer` port and registers Resend in your server. The task generator adds `lib/tasks.ts`, a feature task registry, and `server/tasks.ts`. Keep those registrations. Replace the generated task with this file: ```typescript // features/mail/tasks/send-test.ts import { z } from "zod"; import { defineTask } from "@/lib/tasks"; export const SendTestTaskInputSchema = z.object({ to: z.email(), }); export type SendTestTaskInput = z.infer; export const sendTestTask = defineTask("mail.send-test", { input: SendTestTaskInputSchema, description: "Send a test email to a specified recipient.", async handle({ input, ctx }) { const receipt = await ctx.ports.mailer.send({ to: input.to, subject: "Your Beignet mail setup works", text: "This message was sent through your application's mail port.", }); return { provider: receipt.provider, id: receipt.id }; }, }); ``` The task validates the recipient before calling the mailer. The fixed message keeps this first send focused on verifying your configuration. ## Test without sending mail Add a test beside the feature's other tests: ```typescript // features/mail/tests/send-test.test.ts import { createMemoryMailer } from "@beignet/core/mail"; import { runTask } from "@beignet/core/tasks"; import { createTestContext } from "@beignet/core/testing"; import type { AppContext } from "@/app-context"; import { expect, test } from "@/lib/beignet-test"; import { sendTestTask } from "../tasks/send-test"; const makeContext = createTestContext(); test("sends one message and rejects invalid recipients before delivery", async () => { const mailer = createMemoryMailer({ defaultFrom: "hello@example.com" }); const fixture = makeContext({ ports: { mailer }, ambient: false }); try { const receipt = await runTask(sendTestTask, { input: { to: "reader@example.com" }, ctx: fixture.ctx, }); expect(mailer.deliveries).toHaveLength(1); expect(receipt).toMatchObject({ id: mailer.deliveries[0].id }); expect(mailer.deliveries[0].message.to).toEqual(["reader@example.com"]); expect(mailer.deliveries[0].message.subject).toBe("Your Beignet mail setup works"); await expect(runTask(sendTestTask, { input: { to: "not-an-email" }, ctx: fixture.ctx, })).rejects.toMatchObject({ name: "TaskValidationError" }); expect(mailer.deliveries).toHaveLength(1); } finally { await fixture.dispose(); } }); ``` ```bash bun run format bun run test bun beignet check ``` The test supplies a memory mailer directly, so it needs no Resend credentials and makes no network request. The same task runs against the registered Resend provider when invoked through the CLI. ## Send a real message Add these entries to your existing `.env.local`, replacing both placeholders: ```dotenv RESEND_API_KEY=replace-with-your-resend-api-key RESEND_FROM=Your App ``` Use a domain verified in your Resend account. See the [Resend setup instructions](https://resend.com/docs/send-with-nodejs) if you have not created an API key. Keep the key server-side and out of Git. The CLI task starts your app's configured providers. Make sure the starter's database is migrated before running it: ```bash bun beignet db migrate bun beignet explain task mail.send-test ``` The explanation should identify `mail.send-test` and its source file. Replace `you@example.com` below with a mailbox you control, then run the command once: ```bash bun --bun run beignet task run mail.send-test --input '{"to":"you@example.com"}' --json ``` Expect a successful result with a provider name and message ID. Find that ID in Resend and check your inbox. Provider acceptance means the service accepted the message; it does not prove delivery to the inbox. `--bun` runs the task CLI with Bun so it can resolve the starter's TypeScript path aliases when loading your app. See [Bun's runtime flag](https://bun.sh/docs/runtime#bun). ## Troubleshooting | Symptom | Check | | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | The task cannot be found | Keep the generated feature export and `server/tasks.ts` registration. Run `bun beignet doctor --strict`. | | Startup reports a missing key or sender | Add `RESEND_API_KEY` and `RESEND_FROM` to `.env.local` in the app root. | | Resend rejects the sender | Check domain verification and use an address on that domain. | | The task succeeds but no message arrives | Inspect the message in Resend, including delivery or bounce events, and check spam filtering. | | The request times out | Check the provider's message history before sending again; a timed-out request may already have delivered. | The Resend mail provider does not retry a failed send automatically. Keep business email out of a request's best-effort callback when loss matters. For durable delivery, follow the [background job guide](https://www.beignetjs.com/markdown/guides/background-jobs.md) and design [retry-safe handlers](https://www.beignetjs.com/markdown/jobs.md#retry-safe-jobs). Use [notifications](https://www.beignetjs.com/markdown/notifications.md) when one application event may reach several communication channels. ## Work with your agent Share this page using **Copy page**, then adapt this prompt: ```text Add the send-email guide to this Beignet app. Read the app instructions and installed package guidance first. Inspect existing providers and tasks before generating anything, and preserve custom configuration. Add mail.send-test and test it with the memory mailer: one valid input sends one message, and an invalid recipient sends none. Run format, tests, and beignet check. Leave credentials out of source control and show me the command for sending to my chosen mailbox. ```