---
title: "Email with Microsoft 365"
space: "Frappe Framework"
url: "https://prilk.com/docs/frappe-framework/email-microsoft-365"
updated: "2026-09-07"
---

## Email with Microsoft 365

Microsoft has disabled basic authentication for SMTP and IMAP on Microsoft 365. A mailbox password in an Email Account will not work — mail must authenticate with **OAuth 2.0**, which Frappe drives through a **Connected App**.

This page covers the whole path: app registration in Entra ID, the Connected App, the Email Account, and the authorisation handshake. See [Email Account & Inbox](/docs/frappe-framework/email) for the general email model.

### Before you start
- Entra ID (Azure AD) access that can register an app **and grant admin consent**
- The mailbox's real **UPN** — sometimes different from the address you send as
- **SMTP AUTH enabled** on that mailbox (off by default in many tenants)
- Your site reachable over HTTPS on a stable domain, for the redirect URI

### 1. Register the application
**Entra admin centre → App registrations → New registration**. Name it after whatever will use it, single tenant, and leave the redirect URI empty — it depends on a name that doesn't exist yet (step 5).

From **Overview**, copy the **Application (client) ID** and **Directory (tenant) ID**.

![Entra app registration Overview, showing Application (client) ID and Directory (tenant) ID](/files/11-overview-frappe.png)

### 2. Create a client secret
**Certificates & secrets → New client secret.** Copy the **Value** immediately — it is shown once and never again.

![Certificates and secrets, listing client secrets with their expiry dates](/files/13-client-secrets-frappe.png)

One registration can hold several secrets, and a secret cannot be read back once you leave the page — so give each a description naming what uses it, and note its expiry. Nothing warns you when one lapses.

### 3. Grant the mail permissions
**API permissions → Add a permission → Microsoft Graph → Delegated permissions**:

| Permission | Needed for |
|---|---|
| `SMTP.Send` | Sending |
| `IMAP.AccessAsUser.All` | Receiving |

Then **Grant admin consent** for the tenant. Without consent, the authorisation in step 8 fails.

![API permissions showing delegated SMTP.Send and IMAP.AccessAsUser.All granted](/files/14-api-permissions-frappe.png)

The list may also show **Application** permissions such as `IMAP.AccessAsApp`. Those belong to the service-principal flow and are not what a normal Email Account uses — a delegated token is what authenticates the SMTP session.

### 4. Create the Connected App
**Connected App → New**:

| Field | Value |
|---|---|
| Provider Name | a label, e.g. `M365` |
| Client ID | Application (client) ID from step 1 |
| Client Secret | the secret **Value** from step 2 |
| Authorization URI | `https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize` |
| Token URI | `https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token` |
| Scopes | `https://outlook.office365.com/.default`, `offline_access` |

![Connected App form with client credentials, scopes and endpoints](/files/15-connected-app.png)

Use the Microsoft **v2.0** endpoints. `.default` asks for everything already consented on the registration, which saves re-listing each scope here; naming `SMTP.Send` and `IMAP.AccessAsUser.All` explicitly works too. `offline_access` is what gets you a refresh token — without it the connection dies after an hour.

**Provider Name is only a label.** The document name — shown under the title in the sidebar, a short hash unless you renamed it — is what step 5 depends on.

### 5. Whitelist the redirect URI
Save the Connected App and read the **Redirect URI** field back. Frappe **appends the document name** to the callback path:

```
https://<site>/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/<document-name>
```

Whitelist that exact URI in Azure under **Authentication → Add a platform → Web**.

![Azure Authentication blade listing several redirect URIs, each ending in a Connected App name](/files/12-redirect-uris-frappe.png)

The bare `.../callback` without the trailing name is the most common reason a connect fails. One registration can serve several sites — each Connected App on each site contributes its own callback, as above.

If you rename a Connected App, re-save it so `validate()` recomputes the URI, then whitelist the new one.

### 6. Create the Email Account
**Email Account → New**:

| Field | Value |
|---|---|
| Email Address | the address you send **as** |
| Method | `OAuth` |
| Connected App | the document from step 4 |
| Connected User | the desk user who will authorise |
| Outgoing Server | `smtp.office365.com`, port `587`, TLS |
| Incoming Server | `outlook.office365.com`, port `993`, SSL |

![Email Account form with Method set to OAuth and a Connected App selected](/files/05-email-account.png)

`Connected App` and `Connected User` only appear once **Method** is `OAuth`.

**If the mailbox UPN differs from the sending address** — you send as `info@company.com` but the mailbox is really `info@company.onmicrosoft.com` — tick **Use different Email ID** and put the **UPN** in **Alternative Email ID**. XOAUTH2 authenticates the token identity, not the alias. Get this wrong and you see a `535` failure with a token that is otherwise perfectly valid.

### 7. Share the settings across mailboxes
For more than one mailbox on the same tenant, put the servers and ports in an **Email Domain** once and point every Email Account at it, instead of repeating them.

![Email Domain with Microsoft 365 incoming and outgoing server settings](/files/06-email-domain.png)

### 8. Authorise
Click **Authorize API Access** on the Email Account (or **Connect to \<provider\>** on the Connected App). Sign in as the mailbox and accept the consent screen. That creates a **Token Cache** named `<connected-app>-<connected-user>` — the record the SMTP session actually reads.

![Token Cache record named after the Connected App and the connected user](/files/08-token-cache.png)

This step needs a browser and cannot be scripted. The signed-in user must match **Connected User**, or the session looks for a token cache that does not exist. Each user authorises separately, so a shared mailbox used by several people gets one cache each.

### 9. Verify
Send a test email from the account and confirm its **Email Queue** entry reaches `Sent`. A failure stays in the queue with the error against it — start from the table below.

### Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| `535 5.7.3 Authentication unsuccessful` with a valid token | Logging in as the alias instead of the mailbox UPN | **Use different Email ID** + **Alternative Email ID** (step 6) |
| `AADSTS50011` redirect URI mismatch | Whitelisted URI missing the Connected App's document name | Whitelist `.../callback/<document-name>` (step 5) |
| `AADSTS65001` consent required | Admin consent not granted | Grant admin consent on the delegated permissions (step 3) |
| `invalid_client` | Client secret expired or mistyped | Issue a new secret in Azure, paste it in, save |
| `SmtpClientAuthentication is disabled` | SMTP AUTH off for that mailbox | Enable SMTP AUTH per-mailbox in Exchange |
| Authorises fine but sending fails | **Connected User** does not match the user who authorised | Correct **Connected User**, authorise again |
| Worked for weeks, then everything fails at once | Client secret lapsed | See below |

### Secret renewal
Entra client secrets expire — 24 months at most. Refresh tokens renew silently while the secret is valid, so nothing breaks until it lapses, and then every mailbox on that registration fails together.

You cannot read back which secret a Connected App holds; it is encrypted, and masked in the desk. So at renewal time don't try to identify the one in use — create a fresh secret, paste it into the Connected App, and save. Put the new expiry date in a calendar reminder.
