myagent.mxBLOG

apis & sending · microsoft graph send email

5 Step Checklist for Developers Sending Email With Microsoft Graph

Examples and working code for sending email with Microsoft Graph. C#, Python and raw HTTP snippets, a 5 step auth and mailbox checklist, attachment rules...

10 min read~3,029 tokensMarkdown
Developer wiring a Microsoft Graph sendMail call from a terminal to a mail endpoint

To send email with Microsoft Graph, call POST /me/sendMail for a signed-in user with the delegated Mail.Send scope, or POST /users/{mailbox}/sendMail for app-only flows using the application permission Mail.Send. Both accept JSON or MIME payloads and attachments via fileAttachment with base64-encoded contentBytes, and both return 202 Accepted on success, which confirms Graph took the request, not that the message landed.


TL;DR

5 takeaways
  1. Sending mail requires explicit Mail.Send permission, with delegated flows using POST /me/sendMail and app-only flows using POST /users/{mailbox}/sendMail.
  2. Authentication involves token acquisition through MSAL or direct OAuth, with app registration needing specific permissions and potentially application access policies for mailbox scoping.
  3. Attachments in Graph can be added as base64-encoded contentBytes in JSON or as raw MIME messages, but improper encoding or size can cause errors.
  4. For shared mailboxes and send-as scenarios, additional Exchange Online permissions like Send As or Send on Behalf are necessary, and testing confirms sender identity via Sent Items.
  5. A 202 Accepted response only queues the email; delivery success depends on subsequent processing, so error codes like SendAsDenied or base64 issues need careful handling.

Working code examples for delegated and app-only flows

The fastest way to get mail moving is to pick your auth flow first, then copy the matching snippet.

For a delegated flow, where a signed-in user sends as themselves, authenticate with MSAL using Authorization Code with PKCE (web and mobile apps) or Device Code (CLI tools), then call /me/sendMail:

  • Acquire a token scoped to Mail.Send through MSAL’s acquireTokenInteractive or acquireTokenByDeviceCode.
  • POST the token as a bearer header to https://graph.microsoft.com/v1.0/me/sendMail.
  • Body shape: a message object with subject, body (contentType HTML or Text), toRecipients, and optionally saveToSentItems set to true or false.

For app-only sending, typically from a backend service sending as a shared or service mailbox, use client credentials against https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token with scope https://graph.microsoft.com/.default, then call POST /users/{mailbox}/sendMail with the same JSON body shape.

In C#, the GraphServiceClient wraps this as graphClient.Users["{mailbox}"].SendMail.PostAsync(requestBody), where requestBody is a SendMailPostRequestBody holding the message and a SaveToSentItems flag. In Python, the equivalent through the Microsoft Graph SDK is await user_client.me.send_mail.post(request_body), with request_body built from the SendMailPostRequestBody model.

Raw HTTP is worth knowing even if you use an SDK, because it shows exactly what’s on the wire: a Content-Type: application/json header, a bearer token, and a body with message and saveToSentItems as siblings. A correct call returns 202 Accepted with an empty body, which is where many developers stop checking too early, as detailed in the sendMail API reference.

Authentication, scopes and mailbox scoping checklist

Before writing any code, settle whether you’re building a delegated or app-only integration, because the infrastructure setup diverges early.

  1. Register the app in Microsoft Entra and note the tenant ID and client ID.
  2. For delegated sending, add Mail.Send (and Mail.Send.Shared if the signed-in user sends from a shared mailbox they have access to), request offline_access if you need refresh tokens, and use PKCE for any public client.
  3. For app-only sending, add the Microsoft Graph application permission Mail.Send and have an admin grant tenant-wide consent.
  4. Restrict which mailboxes the app-only token can actually send from using an Application Access Policy or RBAC for Applications in Exchange Online, since application Mail.Send alone has no scoping and can otherwise address every mailbox in the tenant, a point the Microsoft 365 OAuth guidance calls out directly.
  5. Choose a credential type: client secret for quick testing, certificate for anything running in production.

Pro Tip: Use certificate-based authentication instead of client secrets for any app-only integration with broad Mail.Send access, and rotate certificates on a schedule rather than waiting for an expiry alert.

Skipping step four is the single most common misconfiguration teams run into: the app technically works in testing, then someone discovers it can send as the CEO’s mailbox in production.

Attachments and MIME: formats, limits and base64 pitfalls

Graph supports two attachment paths, and which one you need depends on how much control you want over the raw message.

  • The JSON approach adds an attachments array to the message, each item typed as #microsoft.graph.fileAttachment with contentBytes set to the base64-encoded file content, plus a name and contentType.
  • The MIME approach skips the structured message object entirely: you build a full RFC-822 message, base64-encode the whole thing, and POST it as the raw body, which is the route to take when you need exact header control or S/MIME signing, as covered in the guidance on sending MIME messages.

Most malformed-attachment errors trace back to encoding the file path or a data URI prefix instead of the raw bytes. Strip any data: prefix before encoding, and verify the base64 string decodes back to the original file size before you send, as the create-and-send guidance notes when it demonstrates the fileAttachment payload shape. Keep inline attachments modest in size and prefer uploading large files to a share and linking them where your mailbox tooling supports it, rather than inflating every message with megabytes of base64 text.

Personal accounts, shared mailboxes and sending-as scenarios

Personal Microsoft accounts (MSA) authenticate through the /common endpoint rather than a tenant-specific one, and still use the delegated Mail.Send scope once the user consents; /me/sendMail works the same way it does for a work account.

  • Shared mailboxes are where most SendAsDenied errors originate: the app needs Graph permission, but the user or app identity also needs Send As or Send on Behalf (and sometimes Full Access) granted in Exchange Online, a two-layer requirement the guidance on sending as another user spells out explicitly.
  • Use /me/sendMail when the signed-in user is the sender, and /users/{mailbox}/sendMail for app-only calls or when deliberately sending as a different mailbox.
  • Before trusting a new integration, send a test message to a throwaway mailbox and check Sent Items to confirm both the sender identity and whether the copy actually saved where you expected.

Common errors, delivery semantics and troubleshooting

A 202 Accepted response only means Graph queued the request; it is not a delivery receipt, since Exchange Online still has to process, throttle and route the message afterwards, per the sendMail reference.

  1. 401 or 403 responses usually mean the token lacks the right scope or admin consent was never granted; recheck the app registration’s granted permissions.
  2. SendAsDenied means the Exchange-side permission is missing, not the Graph permission, so fix it in Exchange Online rather than Entra.
  3. ErrorMimeContentInvalidBase64String means the MIME or attachment payload wasn’t valid base64, usually from an extra prefix or line-wrapping issue.
  4. For high-volume senders, check MailTips before sending to catch recipient-side constraints such as auto-replies or full mailboxes ahead of time, as the create-and-send documentation recommends.

Pro Tip: If you’re migrating off SMTP, remember plain SMTP typically doesn’t write to Sent Items, while Graph’s sendMail does by default unless you set saveToSentItems to false, so audit that behaviour before you cut over.

Agent mailboxes and why mailbox APIs matter

Everything above assumes you already have a mailbox to send from. For an autonomous agent, that’s often the harder problem: provisioning an inbox, scoping credentials, and wiring up events without a human doing the signing up each time. That’s the gap a Sendmux inbox is built around, handing an agent a working mailbox with scoped keys and webhook delivery instead of a shared Graph app credential everyone has to trust.

What developers get wrong about Graph email

The part of this setup developers underestimate isn’t the API call, it’s the permission model. Teams spend a week wiring up sendMail and then lose two more untangling why a shared mailbox returns SendAsDenied despite a correctly scoped Graph token, because the Exchange-side grant lives in a completely different console.

A scoped token reaches one mailbox while an unrestricted token reaches every mailbox in the tenant

The conventional advice treats 202 Accepted as success and moves on. It isn’t. Treat it as a receipt, not a delivery confirmation, and build a habit of checking Sent Items or tenant delivery logs until you trust the pipeline.

If there’s one thing to prioritise first, it’s deciding delegated versus app-only before writing a line of code, because retrofitting mailbox scoping onto an over-permissioned app-only integration is far more painful than setting an Application Access Policy on day one.

A simpler path for agent mailboxes

When the job is giving an agent its own working inbox rather than wiring a human mailbox into an app registration, Sendmux takes a different route: scoped mailbox keys, webhook or Server-Sent Events delivery, and bring-your-own outbound routing across providers, all billed on usage rather than per seat or per mailbox. Start with a free mailbox at Myagent, or see the full platform at sendmux.ai/product. For managing the inbox once mail starts flowing, theNeedle is worth a look for prioritising what actually needs a reply.

An agent mailbox with scoped keys delivering events over webhooks and streams

FAQ

How do I use Microsoft Graph to get emails?

Call GET /me/messages or GET /users/{id}/messages with the delegated Mail.Read scope or application Mail.Read permission, which returns a JSON collection of message objects you can filter and page through.

Is Microsoft Graph still available?

Yes, Microsoft Graph is Microsoft’s current API for Outlook, Exchange Online and Microsoft 365 data, and it’s the recommended path over legacy protocols for new mail integrations, including sending as detailed in the sendMail reference.

Is there a way to automate sending emails?

Yes: automate sending through Microsoft Graph’s sendMail endpoint with either a delegated or app-only token, or continue using SMTP with OAuth2 (AUTH XOAUTH2) if your infrastructure still depends on it, per Microsoft’s SMTP OAuth guidance. Agent-based workflows that need a dedicated inbox rather than a shared app credential can also provision one through a Sendmux inbox.

How do I send an email from Microsoft?

Send from a Microsoft 365 or personal account by calling POST /me/sendMail with a valid access token carrying the Mail.Send scope, passing a JSON message object with recipients, subject and body.

Sources

Give an agent its own address

Sendmux is the Email Inbox API for AI Agents.

Explore Sendmux