# Microsoft Graph (https://email-sdk.dev/docs/adapters/graph)



## Capabilities

| Repeated headers | Idempotency | Scheduling | Personalized |
| ---------------- | ----------- | ---------- | ------------ |
| Yes              | `none`      | No         | `expanded`   |

These values come from the adapter's exported `capabilities` declaration. The [field support matrix](https://email-sdk.dev/docs/adapters/field-support) covers normalized message fields.

The Graph adapter calls Microsoft Graph's [sendMail API](https://learn.microsoft.com/en-us/graph/api/user-sendmail) with plain `fetch` and supports both OAuth 2.0 client credentials flow and custom `getAccessToken` functions for managed identity, certificate-based authentication, or external token providers. It authenticates with app permissions (`Mail.Send`) and sends from a specific user's mailbox by user ID or UPN.



**[Microsoft Graph](https://learn.microsoft.com/en-us/graph/api/user-sendmail)** · `@opencoredev/email-sdk/graph` · [setup guide](https://email-sdk.dev/docs/adapters/graph) · [live check](https://email-sdk.dev/docs/adapters/verification)



## Configure

Create an app registration in Microsoft Entra ID (Azure AD), grant it the `Mail.Send` application permission, get admin consent, and verify the sender identity matches a real mailbox in your tenant.

```ts title="lib/email.ts"
import { createEmailClient } from "@opencoredev/email-sdk";
import { graph } from "@opencoredev/email-sdk/graph";

export const email = createEmailClient({
  adapters: [
    graph({
      tenantId: process.env.MS_GRAPH_TENANT_ID!,
      clientId: process.env.MS_GRAPH_CLIENT_ID!,
      clientSecret: process.env.MS_GRAPH_CLIENT_SECRET!,
      user: process.env.MS_GRAPH_USER!,
    }),
  ],
});
```



| Option | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| tenantId | `string` | No |  | Microsoft Entra tenant ID. Required unless using getAccessToken. |
| clientId | `string` | No |  | Application (client) ID from your app registration. Required unless using getAccessToken. |
| clientSecret | `string` | No |  | Client secret value from your app registration. Required unless using getAccessToken. |
| user | `string` | Yes |  | User ID (GUID) or UPN (email) of the mailbox to send from, e.g. d4f540b2-6478-4ee3-bdd3-d3a5397d97ac or john.doe@contoso.com. |
| getAccessToken | `() => string \| Promise<string>` | No |  | Custom token provider for managed identity, certificate credentials, or external caching. Skips the built-in client credentials flow. Mutually exclusive with tenantId/clientId/clientSecret. |
| baseUrl | `string` | No | `"https://graph.microsoft.com/v1.0"` | Override the Graph API origin for national clouds. |
| tokenUrl | `string` | No | `"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token"` | Override the OAuth token endpoint. |
| scope | `string` | No | `"https://graph.microsoft.com/.default"` | OAuth scope for client credentials. For national clouds, use the Graph service origin followed by /.default. |
| tokenTimeoutMs | `number` | No | `30000` | Maximum time to wait for the built-in client-credentials token request, including its response body. |
| saveToSentItems | `boolean` | No | `true` | Store a copy in the sender's Sent Items folder. |
| fetch | `typeof fetch` | No |  | Custom fetch implementation for tests or special runtimes. |



## Send

Graph maps `cc`, `bcc`, one `replyTo`, custom headers (x- prefix only), and base64 attachments. There are no tags, metadata, or scheduled delivery.

```ts
const result = await email.send({
  from: "Acme Alerts <alerts@acme.com>",
  to: "ops@example.com",
  subject: "Build failed on main",
  html: "<p>Commit abc123 broke the build.</p>",
  headers: [{ name: "X-Build-ID", value: "build_98412" }],
});

console.log(result.adapter); // "graph"
```

Graph returns a `202 Accepted` status with an empty body, so there is no message ID. If you need to correlate delivery events later, keep `saveToSentItems` enabled and retrieve the `internetMessageId` from the Sent Items copy.

>
> **Only x- prefixed custom headers**
>
>   Graph accepts custom headers only if the name starts with `x-` (case insensitive). A header like
>   `List-Unsubscribe` throws an `EmailValidationError` before any request is made. Graph caps custom
>   headers at 5 per message.
>

>
> **No tags, metadata, or scheduling**
>
>   Graph's `sendMail` API has no `tags`, `metadata`, or `sendAt` fields. Pass any of these and the
>   adapter throws an `EmailValidationError` before making a request.
>

## Managed identity and certificate authentication

Pass a custom `getAccessToken` function to skip the built-in client credentials flow. The adapter calls your function before every send and caches nothing, so you control token lifetime and refresh logic.

```ts
import { DefaultAzureCredential } from "@azure/identity";
import { graph } from "@opencoredev/email-sdk/graph";

const credential = new DefaultAzureCredential();

export const email = createEmailClient({
  adapters: [
    graph({
      user: "alerts@acme.com",
      async getAccessToken() {
        const token = await credential.getToken("https://graph.microsoft.com/.default");
        return token.token;
      },
    }),
  ],
});
```

When `getAccessToken` is supplied, `tenantId`, `clientId`, and `clientSecret` must not be set.

## Verify from the CLI

Export `MS_GRAPH_CLIENT_SECRET` in your shell or inject it through your secret manager.
The CLI reads it from the environment; do not pass it as a command-line argument.

```bash
MS_GRAPH_TENANT_ID="..." MS_GRAPH_CLIENT_ID="..." MS_GRAPH_CLIENT_SECRET="..." MS_GRAPH_USER="..." \
  npx --package @opencoredev/email-sdk email-sdk doctor --adapter graph
```

```bash
npx --package @opencoredev/email-sdk email-sdk send \
  --adapter graph \
  --tenant-id "$MS_GRAPH_TENANT_ID" \
  --client-id "$MS_GRAPH_CLIENT_ID" \
  --user "$MS_GRAPH_USER" \
  --from "Acme <hello@acme.com>" \
  --to user@example.com \
  --subject "Graph smoke test" \
  --text "It works" \
  --dry-run
```

Drop `--dry-run` for one real send. Only Microsoft can prove the app registration, consent, and mailbox are set up correctly.

## National clouds and Convex

Add `--live` to `doctor --adapter graph` for a non-sending client-credentials check.
It verifies authentication, not mailbox access, `Mail.Send` consent, or delivery.
The CLI accepts `--token-url` / `MS_GRAPH_TOKEN_URL` and `--scope` / `MS_GRAPH_SCOPE`
for national-cloud authentication, alongside `--base-url` / `MS_GRAPH_BASE_URL` for sending.

For a national-cloud SDK client, configure `baseUrl`, `tokenUrl`, and `scope` together.
For example, US Government L4 uses `https://graph.microsoft.us/v1.0`,
`https://login.microsoftonline.us/<tenant-id>/oauth2/v2.0/token`, and
`https://graph.microsoft.us/.default`. See [Microsoft's national-cloud guidance](https://learn.microsoft.com/en-us/graph/sdks/national-clouds).

In the Convex component, endpoint overrides and scope are server-controlled through
`MS_GRAPH_BASE_URL`, `MS_GRAPH_TOKEN_URL`, and `MS_GRAPH_SCOPE`. Public send requests
cannot provide literal endpoint URLs, so callers cannot redirect server credentials.
`MS_GRAPH_CLIENT_SECRET` is restricted to Graph's `clientSecret` field; other adapter
or field environment overrides cannot read it.
Graph's environment mappings are fixed to its declared variables. Map custom secret
names in the trusted `app.use(convexEmail, { env })` configuration, not in send requests.


## Frequently asked questions

### Does Email SDK support Microsoft Graph?

Yes. Email SDK ships a Microsoft Graph adapter imported from @opencoredev/email-sdk/graph. You keep your Microsoft Graph account and credentials; the SDK adds message validation, typed errors, and no-network test adapters around the same send() call used for every other provider.

### Which message fields does the Microsoft Graph adapter support?

Microsoft Graph supports CC recipients, BCC recipients, Reply-To address, Custom headers, and Attachments. It does not support Tags, Metadata, and Scheduled sending (sendAt); Email SDK rejects a message that uses those fields before any request is made.

### Can Microsoft Graph schedule email for later with Email SDK?

No. Microsoft Graph has no provider-side scheduling, so a message with sendAt fails validation. Store the job in your own queue and send when it is due.

### Does the Microsoft Graph adapter support idempotent sends?

No. Microsoft Graph has no provider-side idempotency, so a retried send can be delivered twice. Deduplicate in your own queue before retrying.
