# Test and recover (/docs/integrations/convex/testing-and-recovery)



## Redirect every recipient [#redirect-every-recipient]

Test mode changes the stored message before it is queued. It replaces `to`, removes `cc` and `bcc`, and adds `metadata.convexEmailTestMode: true`.

```ts title="convex/emailAdmin.ts"
await email.setConfig(ctx, {
  defaultFrom: "Acme <hello@acme.com>",
  testMode: true,
  sandboxTo: ["email-preview@acme.com"],
  maxAttempts: 2,
  retryBaseMs: 1_000,
  cleanupAfterDays: 7,
});
```

`setConfig` replaces the whole document, so include every value you want to keep. Test mode without `sandboxTo` fails before enqueueing and cannot contact the original recipients.

## Use in-memory delivery [#use-in-memory-delivery]

```ts title="convex/email.ts"
export const email = new ConvexEmail(components.convexEmail, {
  adapters: [{ kind: "memory" }],
  defaultAdapter: "memory",
});
```

The memory adapter exercises the real queue, scheduler, status, event, idempotency, cancellation, and cleanup behavior without a network call.

For `convex-test`, register the packaged component helper:

```ts title="convex/test.setup.ts"
import convexEmailTest from "@opencoredev/convex-email/test";
import { convexTest } from "convex-test";

export function createTest() {
  const t = convexTest();
  convexEmailTest.registerConvexEmail(t);
  return t;
}
```

## Read the failure before retrying [#read-the-failure-before-retrying]

```ts
const status = await email.status(ctx, { emailId });
const events = await email.listEvents(ctx, { emailId });
```

Check `lastError`, `attemptCount`, `attemptedAdapters`, and the ordered events. Fix missing credentials, invalid sender configuration, unsupported message fields, or provider policy errors before starting another cycle.

## Retry a terminal failure [#retry-a-terminal-failure]

```ts title="convex/emailAdmin.ts"
import { v } from "convex/values";
import { internalMutation } from "./_generated/server";
import { email } from "./email";

export const retry = internalMutation({
  args: { emailId: v.string() },
  returns: v.boolean(),
  handler: async (ctx, args) => email.retry(ctx, args),
});
```

`retry` returns `true` only for a terminal `failed` record. It resets the attempt counter, clears the terminal error, appends a manual `retry_scheduled` event, and queues processing immediately. Sent and canceled messages are not retryable.

Use an idempotency key on the original message. Automatic recovery refuses to retry stale in-flight sends without one because the provider may already have accepted the message before the worker lost its result.

## Expose only authorized operations [#expose-only-authorized-operations]

`exposeApi()` can generate app function definitions for send, batch, status, events, cancellation, and retry. Configuration functions stay excluded unless `includeConfigApi: true`.

Most applications should write explicit wrappers instead. That keeps authentication, tenant checks, recipient policy, and administrator-only recovery visible in application code.

<Card title="Send and track email" href="/docs/integrations/convex/sending-and-status" description="Understand queue state and event history." />

<Card title="Test core Email SDK" href="/docs/guides/test-email-behavior" description="Use memory and failing adapters outside Convex." />
