Microsoft 365 OAuth Mailer
Routes all WordPress wp_mail() email through Microsoft 365 using OAuth 2.0 (Modern Authentication) and the Microsoft Graph API. No basic SMTP authentication, no paid third-party mail service.
by Manpreet Singh · github.com/manpreetdev21/microsoft-365-oauth-mailer · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/manpreetdev21/microsoft-365-oauth-mailer/archive/refs/heads/main.zipReadme
Microsoft 365 OAuth Mailer
Contributors: Manpreet Singh Tags: microsoft 365, office 365, oauth, smtp, microsoft graph Requires at least: WordPress 5.9 Tested up to: WordPress 6.9 Requires PHP: 8.0 (tested on 8.1, 8.2, 8.3, 8.4) Stable tag: 1.0.0 License: GPLv2 or later (license text)
Send all WordPress email through Microsoft 365 with OAuth 2.0 modern authentication and the Microsoft Graph API. No basic SMTP authentication.
Description
Microsoft 365 OAuth Mailer intercepts wp_mail() and delivers the message with the Microsoft Graph sendMail endpoint using a delegated OAuth 2.0 access token.
- No SMTP username or password anywhere.
- No paid third-party mail service, no Composer dependency, no external HTTP library.
- Microsoft Entra ID and Microsoft Graph are the only external services contacted.
- Access and refresh tokens plus the client secret are encrypted at rest (Sodium, with an OpenSSL AES-256-GCM fallback).
- Works transparently with anything that calls
wp_mail(): Contact Form 7, WooCommerce, password resets, core notifications and custom plugins. - If Microsoft 365 is not connected, WordPress keeps using its normal mail process. Installing the plugin never breaks an unconfigured site.
Email flow
wp_mail()
-> pre_wp_mail interception
-> OAuth 2.0 access token (refreshed automatically)
-> POST https://graph.microsoft.com/v1.0/me/sendMail
-> Microsoft 365
Admin pages
The plugin adds a top-level M365 Mailer menu to the WordPress sidebar. Each section is a real admin page with its own URL, reachable from the sidebar or from the tab bar at the top of the screen:
| Page | URL | What it does |
|---|---|---|
| Settings | admin.php?page=m365om |
Entra credentials, redirect URI, mail options, setup reference. |
| Send Test | admin.php?page=m365om-test |
Sends a test email and shows exactly what will be sent. |
| Logs | admin.php?page=m365om-logs |
The last 200 OAuth, token and Graph events, newest first, colour coded by severity, with a Clear log button. |
| Setup Guide | admin.php?page=m365om-help |
Where each Microsoft key lives, the app registration walkthrough, how sending works, plugin and system details, and a plain-language error reference. |
The connection status and the Connect / Disconnect button sit in the header bar, so they are available from every page.
Requested permissions
Only what is needed:
| Scope | Why |
|---|---|
openid, profile, email |
Identifies the account that signs in (used for the connected-account display). |
offline_access |
Allows the plugin to refresh the access token without asking the administrator to sign in again. |
https://graph.microsoft.com/Mail.Send (delegated) |
Permission to send mail as the signed-in user. |
No other Microsoft Graph permission is requested.
Security Defaults
This plugin uses OAuth 2.0 / modern authentication and the Microsoft Graph API. Microsoft Entra Security Defaults do not need to be disabled, SMTP AUTH does not need to be enabled on the mailbox, and no basic username/password authentication is used at any point.
That said, OAuth does not override tenant policy. Conditional access rules, authentication policies, licensing, mailbox permissions and Exchange Online mail-flow restrictions can still prevent an account from sending. If sending is blocked, the error message and Graph error code shown by the plugin identify which of those applies.
Installation
- In WordPress, go to Plugins → Add New → Upload Plugin.
- Choose
microsoft-365-oauth-mailer.zipand click Install Now. - Click Activate.
- Open the new M365 Mailer item in the WordPress admin sidebar.
- Complete the Microsoft Entra app registration described below, enter the credentials, and click Connect Microsoft 365.
Manual installation: upload the microsoft-365-oauth-mailer folder to wp-content/plugins/ and activate it from the Plugins screen.
Microsoft Entra App Registration
The same walkthrough is built into the plugin, on M365 Mailer → Setup Guide, where it also shows this site's exact redirect URI and which values you have already saved.
-
Sign in to the Microsoft Entra admin center as an administrator (the Azure portal works too).
-
Open Identity → Applications → App registrations and click New registration.
-
Give it a name, for example
WordPress Mailer - example.com. -
Under Supported account types, choose the option that matches your organisation. For a normal company tenant this is Accounts in this organizational directory only (Single tenant).
-
Under Redirect URI, select the platform Web and paste the redirect URI shown on the plugin settings page:
https://example.com/wp-admin/admin-post.php?action=m365om_oauth_callbackIt must match character for character, including
https, the host name, and any subdirectory. Use the Copy button on the settings page rather than typing it. -
Click Register.
-
On the Overview page, copy the Directory (tenant) ID and the Application (client) ID.
-
Open Certificates & secrets → Client secrets → New client secret. Choose an expiry, click Add, then immediately copy the Value column. The Value is only shown once — do not copy the Secret ID.
-
Open API permissions → Add a permission → Microsoft Graph → Delegated permissions, search for
Mail.Send, tick it and click Add permissions. -
If your tenant requires it, click Grant admin consent for <tenant>. Many tenants disable user consent, in which case this step is mandatory.
-
Do not add
Mail.Sendas an Application permission for this plugin; it uses the delegated flow.
WordPress Configuration
The plugin adds its own top-level M365 Mailer menu with three pages: Settings, Send Test and Logs. On the Settings page:
| Field | What to enter |
|---|---|
| Tenant ID | The Directory (tenant) ID GUID from the Overview page. common, organizations, consumers or a verified domain such as contoso.onmicrosoft.com are also accepted. |
| Client ID | The Application (client) ID GUID from the Overview page. |
| Client Secret | The client secret Value. It is encrypted before being stored and is never displayed again. Leave the field empty when saving other settings to keep the stored secret. |
| Redirect URI | Read-only, generated from your site URL. Copy it into the App Registration. |
| Mailbox | Informational in version 1. Microsoft Graph /me/sendMail always sends as the account that signed in. |
| From Name / From Email | Microsoft will not allow this account to send as a different address, so the From Email is applied as Reply-To when an email does not already set one. |
| Sent Items | Keep a copy of each message in the mailbox Sent Items folder. |
| Enable Debug Logging | Records events on the Logs page and writes [M365OM] lines to the PHP error log. Secrets, tokens and authorization codes are never logged; OAuth and Graph error codes are kept, since those are what you need to diagnose a failure. |
Save the settings, then click Connect Microsoft 365, sign in with the mailbox that should send email, and accept the consent prompt. The status box then shows the connected account.
Testing
- Open M365 Mailer → Send Test.
- Enter a recipient address and click Send Test Email.
- A success notice means Microsoft Graph accepted the message (HTTP 202). Any failure is reported with the Graph error code and request ID.
To test a real integration, submit a Contact Form 7 form, trigger a WooCommerce order email, or use Lost your password? on the login screen. Anything that calls wp_mail() is routed automatically:
wp_mail(
'user@example.com',
'Invoice',
'<p>Please find the invoice attached.</p>',
array( 'Content-Type: text/html' ),
array( '/path/to/invoice.pdf' )
);
To watch what happens under the hood, turn on Enable Debug Logging on the Settings page and open the Logs page after sending.
A standalone self-check for the pure logic (encryption round-trip, address and header parsing, message building, attachment limits, write-only secret handling, log redaction and capping) ships with the plugin and needs no WordPress bootstrap:
php tests/test-m365om.php
Development
The plugin folder is the repository root, so wp-content/plugins/microsoft-365-oauth-mailer/ can be cloned straight into a WordPress install and activated with no build step: no Composer, no npm, no compiled assets.
.gitignore keeps build artifacts (*.zip), stray dependency folders, logs and editor/OS files out of the repository. Nothing in it is required at runtime, and dot-files are excluded from the distributed ZIP as well.
microsoft-365-oauth-mailer/
├── microsoft-365-oauth-mailer.php bootstrap
├── uninstall.php removes every option this plugin owns
├── includes/ one class per responsibility
├── assets/css, assets/js admin UI, no external requests
├── languages/ translation target
└── tests/test-m365om.php standalone self-check
Troubleshooting
| Symptom | Cause and fix |
|---|---|
invalid_client / AADSTS7000215 |
The client secret is wrong. Most often the Secret ID was pasted instead of the secret Value. Create a new secret and paste the Value. |
| AADSTS7000222 | The client secret has expired. Create a new one under Certificates & secrets and save it in the plugin. |
redirect_uri_mismatch / AADSTS50011 |
The redirect URI in Entra does not exactly match the one on the settings page. Copy it again with the Copy button. Watch for http vs https, www vs no www, and a changed site URL. |
| AADSTS900023 / invalid tenant | The Tenant ID is wrong. Use the Directory (tenant) ID GUID from the Overview page. |
| AADSTS65001 — consent required | Nobody has consented to Mail.Send. Grant admin consent in API permissions, then reconnect. |
| AADSTS50076 / AADSTS53003 | MFA or a conditional access policy blocks this sign-in. Complete the interactive sign-in from a compliant device or network, or adjust the policy for this application. |
| "The Microsoft sign-in request expired or was already used" | The OAuth state is valid for 10 minutes and once only. Click Connect Microsoft 365 again. |
| HTTP 401 after working previously | The access token expired and the refresh failed. The plugin retries once automatically; if it still fails the connection is marked invalid and you must reconnect. Password changes, revoked sessions and removed consent all invalidate refresh tokens. |
HTTP 403 — ErrorAccessDenied |
The Mail.Send delegated permission is missing or unconsented, or a tenant policy or Exchange Online restriction blocks this mailbox from sending. Confirm the permission is delegated and consented, and that the mailbox is not blocked (for example by an outbound spam policy). |
HTTP 404 — MailboxNotEnabledForRESTAPI / ErrorItemNotFound |
The signed-in account has no Exchange Online mailbox, or its licence does not include one. Sign in with an account that has a mailbox. |
| HTTP 429 | Microsoft is throttling the mailbox. The plugin honours a short Retry-After once and then reports the failure. High-volume sites should throttle their own sending. |
| HTTP 500 / 503 | A Microsoft-side outage. The plugin retries once, then reports the failure. |
| "could not be encrypted" | The server has neither the Sodium extension nor OpenSSL with AES-256-GCM. Enable one of them; almost every PHP 8 build ships Sodium. |
| Attachment too large | Version 1 sends attachments inline, which Microsoft Graph limits to under 3 MB per message. Larger files need an upload session (planned for version 2). |
Known Limitations (V1)
- Sends only as the connected account (
/me/sendMail). Shared mailboxes, Send As and Send on Behalf are not supported yet. - Attachments must be under 3 MB each and 3 MB in total.
- No
multipart/alternative: an email is sent as either HTML or plain text, matching theContent-Typeheader. - Custom headers are limited to five
X-prefixed headers, which is the Microsoft Graph limit. - Emails are sent synchronously during the request, exactly as
wp_mail()normally behaves. There is no queue.
Frequently Asked Questions
Does this need WP Mail SMTP or any paid service? No. Microsoft Entra ID and Microsoft Graph are the only external services used.
What happens if the connection breaks? If the refresh token becomes permanently invalid, the plugin stops retrying, marks the connection as disconnected and shows an admin notice asking you to reconnect. It does not silently swallow the error and it does not loop.
What happens if the plugin is installed but not connected?
wp_mail() behaves exactly as it did before. The interception only takes effect once a connection exists.
Which hooks does it expose?
| Hook | Purpose |
|---|---|
m365om_should_send (filter) |
Return false to let WordPress handle a specific email normally. |
m365om_graph_message (filter) |
Modify the Graph message resource before it is sent. |
m365om_send_mail_endpoint (filter) |
Override the sendMail endpoint (the V2 seam for shared mailboxes). |