# SendGrid

Lightweight, dependency-free, in-memory SendGrid v3 Web API fake for testing code that uses the real `@sendgrid/mail` client.

Default port: `4650`

## Implemented Operations

### Mail (the surface `@sendgrid/mail` actually calls)

- `POST /v3/mail/send` — accepts a SendGrid v3 mail payload, validates it like the real API, captures it in memory, and returns `202 Accepted` with an empty body and an `X-Message-Id` response header. This is the single endpoint that `@sendgrid/mail`'s `send()`, `send([...])`, and `sendMultiple()` hit.
- `POST /v3/mail/batch` — creates a `batch_id` for scheduled/batched sends (`201`).
- `GET /v3/mail/batch/:batch_id` — returns batch status (`200`).

### Account / key management (broader `@sendgrid/client` surface)

- `GET /v3/scopes` — lists the scopes available to the authenticated key.
- `GET /v3/api_keys` — lists API keys (`{ result: [...] }`).
- `POST /v3/api_keys` — creates an API key; returns the full `api_key` secret once (`201`).
- `GET /v3/api_keys/:id` — retrieves an API key (name + scopes).
- `PUT|PATCH /v3/api_keys/:id` — updates an API key's name/scopes.
- `DELETE /v3/api_keys/:id` — deletes an API key (`204`).

### Unsubscribe groups & suppressions (ASM)

- `GET /v3/asm/groups` — lists unsubscribe groups.
- `POST /v3/asm/groups` — creates a group (`201`).
- `GET /v3/asm/groups/:id` — retrieves a group.
- `PATCH|PUT /v3/asm/groups/:id` — updates a group.
- `DELETE /v3/asm/groups/:id` — deletes a group (`204`).
- `POST /v3/asm/suppressions/global` — adds global unsubscribes (`{ recipient_emails: [...] }`, `201`).
- `GET /v3/asm/suppressions/global/:email` — checks a global unsubscribe (`{ recipient_email }` or `{}`).
- `DELETE /v3/asm/suppressions/global/:email` — removes a global unsubscribe (`204`).

### Sender verification

- `GET /v3/verified_senders` — lists verified senders (`{ results: [...] }`).
- `POST /v3/verified_senders` — registers a verified sender (`201`, auto-`verified: true`).

### Service & inspection operations

- `GET /` — returns service metadata.
- `GET /health` — returns `{ "status": "ok" }`.
- `OPTIONS *` — returns `204` (CORS preflight).
- `GET /__parlel/messages` — lists every captured outbound message (`{ messages, count }`).
- `GET /__parlel/messages/:message_id` — returns one captured message.
- `DELETE /__parlel/messages` — clears the captured mailbox only.
- `POST /__parlel/reset` — clears all in-memory state.
- `server.reset()` — clears all in-memory state when used in-process.

## Quick Start

The fake speaks the exact wire protocol of the real client. Point `@sendgrid/mail`
at the local server by overriding the client base URL:

```js
import sgMail from "@sendgrid/mail";
import { SendgridServer } from "./services/sendgrid/src/server.js";

const server = new SendgridServer(4650);
await server.start();

sgMail.setApiKey("SG.parlel");
// Route the client to the local fake instead of api.sendgrid.com:
sgMail.client.setDefaultRequest("baseUrl", "http://127.0.0.1:4650");

const [response] = await sgMail.send({
  to: "test@example.com",
  from: "verified@parlel.dev",
  subject: "Sending with parlel is fun",
  text: "and easy to do anywhere, even with Node.js",
  html: "<strong>and easy to do anywhere, even with Node.js</strong>",
});

console.log(response.statusCode); // 202
console.log(response.headers["x-message-id"]);

await server.stop();
```

To assert what was "sent" in a test, read the captured mailbox:

```js
const res = await fetch("http://127.0.0.1:4650/__parlel/messages");
const { messages, count } = await res.json();
```

## Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.

| Feature | Status | Notes |
| --- | --- | --- |
| `@sendgrid/mail` `send` / `sendMultiple` / `send([...])` | Supported | All route through `POST /v3/mail/send`. |
| `setApiKey` (Bearer auth) | Supported | Any non-empty Bearer token is accepted. |
| `setTwilioEmailAuth` (Basic auth) | Supported | Any non-empty Basic credential is accepted. |
| Mail payload validation | Supported | Validates `personalizations`, `to`, `from`, `subject`, and `content` with real SendGrid error envelopes. |
| Template sends (`template_id`) | Supported | `subject` and `content` become optional when a template is used. |
| `cc` / `bcc` / `reply_to` / attachments / categories / `custom_args` / `asm` / settings | Stored only | Preserved verbatim in the captured message; not interpreted. |
| `202` + empty body + `X-Message-Id` | Supported | Matches the real mail/send response exactly. |
| API keys, ASM groups, global suppressions, verified senders, scopes, mail batch | Supported | Common `@sendgrid/client` companion endpoints. |
| Message capture / inspection | Supported | `/__parlel/messages` exposes everything that was sent. |
| Actual email delivery | Unsupported | Nothing leaves the process — zero side effects by design. |
| Event/Inbound Parse webhooks | Unsupported | No outbound callbacks are made. |
| Stats, Marketing Campaigns, Contacts, Templates CRUD | Unsupported | Outside the `@sendgrid/mail` surface; not required for app tests. |
| Persistence | Unsupported | State is ephemeral by design. |
| Rate limiting / quotas | Unsupported | Local tests should not pay SendGrid costs or hit side effects. |

## Error Shapes

All JSON errors use SendGrid v3 framing — an `errors` array where each entry has
`message`, `field`, and `help`:

```json
{
  "errors": [
    {
      "message": "The subject is required. You can get around this requirement if you use a template with a subject defined or if every personalization has a subject defined.",
      "field": "subject",
      "help": "http://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/errors.html#message.subject"
    }
  ]
}
```

Returned status codes:

| Status | When |
| --- | --- |
| `202` | `POST /v3/mail/send` accepted (empty body, `X-Message-Id` header). |
| `200` | Successful reads / list operations. |
| `201` | Resource created (api key, asm group, global suppression, verified sender, mail batch). |
| `204` | Successful delete / CORS preflight. |
| `400` | Validation failure (invalid/missing mail fields, missing required `name`, malformed JSON body). |
| `401` | Missing or unrecognized `Authorization` header. |
| `404` | Unknown endpoint or missing resource. |
| `405` | Endpoint exists but the HTTP method is unsupported. |
| `500` | Unexpected server exception. |
