WP Manifestindependent plugin directory
manifest / email / microsoft-365-oauth-mailer

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

0stars
0forks

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.zip

Readme

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

  1. In WordPress, go to Plugins → Add New → Upload Plugin.
  2. Choose microsoft-365-oauth-mailer.zip and click Install Now.
  3. Click Activate.
  4. Open the new M365 Mailer item in the WordPress admin sidebar.
  5. 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.

  1. Sign in to the Microsoft Entra admin center as an administrator (the Azure portal works too).

  2. Open Identity → Applications → App registrations and click New registration.

  3. Give it a name, for example WordPress Mailer - example.com.

  4. 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).

  5. 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_callback

    It 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.

  6. Click Register.

  7. On the Overview page, copy the Directory (tenant) ID and the Application (client) ID.

  8. 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.

  9. Open API permissions → Add a permission → Microsoft Graph → Delegated permissions, search for Mail.Send, tick it and click Add permissions.

  10. If your tenant requires it, click Grant admin consent for <tenant>. Many tenants disable user consent, in which case this step is mandatory.

  11. Do not add Mail.Send as 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

  1. Open M365 Mailer → Send Test.
  2. Enter a recipient address and click Send Test Email.
  3. 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 the Content-Type header.
  • 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).

Changelog

Read the full README on GitHub →