wapplersystems / microsoft-graph-mailer
Microsoft Graph mailer transport for TYPO3 — sends emails via Microsoft 365 Graph API with OAuth2 (uses wapplersystems/oauth-service)
Package info
github.com/WapplerSystems/t3-microsoft-graph-mailer
Type:typo3-cms-extension
pkg:composer/wapplersystems/microsoft-graph-mailer
Requires
- php: >=8.2
- symfony/mailer: ^7.0
- symfony/mime: ^7.0
- typo3/cms-core: ^14
- wapplersystems/oauth-service: ^14.1
This package is auto-updated.
Last update: 2026-06-04 03:04:04 UTC
README
Sends TYPO3 system emails via the Microsoft Graph API using OAuth 2.0
client credentials. Built on top of
wapplersystems/oauth-service
for token acquisition, caching, refresh and BE-configurable client setup.
No SMTP. No mailbox password. No interactive user consent needed.
Why
Office 365 / Exchange Online is phasing out Basic Authentication for SMTP
AUTH. Modern delivery uses the Microsoft Graph sendMail endpoint with
an OAuth2 bearer token. This extension wires that endpoint into TYPO3's
existing Symfony Mailer pipeline so any MailerInterface consumer sends
through Graph transparently.
Requirements
- TYPO3 v14
- PHP 8.2+
wapplersystems/oauth-service≥ release withtx_oauthsvc_client.metadata- A Microsoft 365 tenant + Entra ID app registration with the
Mail.SendApplication permission (admin-consent granted) - A dedicated service mailbox restricted via Application Access Policy
Installation
composer require wapplersystems/microsoft-graph-mailer typo3 extension:setup
Configuration
1. Azure / Microsoft Entra ID — App registration
- https://entra.microsoft.com → Applications → App registrations → New registration
- Name:
TYPO3 Mailer – yourdomain.com - Account type: Single tenant
- Redirect URI: leave empty (app-only flow, no user login)
- Name:
- API permissions → Microsoft Graph → Application permissions
- Add
Mail.Send - Click Grant admin consent
- Add
- Certificates & secrets → New client secret
- Copy the Value (only shown once)
- Note from the Overview page:
- Directory (tenant) ID
- Application (client) ID
2. Restrict the app to one mailbox (Exchange Online PowerShell)
Without this step the app could send from any mailbox in the tenant.
Connect-ExchangeOnline New-DistributionGroup -Name "TYPO3MailerAllowedSenders" ` -Type Security -Members noreply@yourdomain.com New-ApplicationAccessPolicy ` -AppId <CLIENT-ID> ` -PolicyScopeGroupId TYPO3MailerAllowedSenders@yourdomain.com ` -AccessRight RestrictAccess ` -Description "TYPO3 Mailer restricted to noreply mailbox"
3. TYPO3 — OAuth Services backend module
Go to System → OAuth Services → Clients → New:
| Field | Value |
|---|---|
| Provider | microsoft_graph |
| Active | ✅ |
| Client ID | Application (client) ID from Azure |
| Client Secret | Client secret value from Azure |
| Scopes | https://graph.microsoft.com/.default |
| Metadata (JSON) | {"tenant_id": "<directory tenant id>", "sender_upn": "noreply@yourdomain.com"} |
Save. The token is fetched lazily on the first mail send and cached by
oauth-service (TYPO3 oauth_service cache backend) for its expires_in
lifetime.
4. Wire the transport
Add to config/system/additional.php (or set via Admin Tools → Settings →
Configure Installation-wide Options → MAIL):
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport'] = \WapplerSystems\MicrosoftGraphMailer\Mailer\GraphTransport::class;
That is the only TYPO3-side configuration. No DSN, no SMTP settings, no
mailbox password. The transport reads sender_upn and tenant_id from
the OAuth client's metadata at send time.
5. (Recommended) Scheduler
oauth-service ships two console commands. For client-credentials only
the cache invalidation matters, but the monitor catches misconfigured
credentials early:
typo3 oauth-service:monitor-connections # daily
Testing
typo3 microsoft-graph-mailer:test you@example.com
A successful send returns silently. On error the transport throws a
GraphMailerException with the Microsoft request-id header — quote
that in any Microsoft support case.
Failure handling — the spool fallback
To avoid silently dropping emails when Microsoft Graph rejects a request
(no OAuth client configured, expired secret, license issue, 401/403/404
from sendMail, …), the transport writes the original message as an
.eml file plus a .json payload record to a spool directory and lets
the caller proceed. Default location:
<typo3_var>/typo3-mail-spool/
Each undelivered message becomes a pair:
YYYYMMDD-HHMMSS-recipient_at_domain-xxxxxxxx.eml— full RFC 5322 message; openable in any mail client, useful for human inspection or manual resendYYYYMMDD-HHMMSS-recipient_at_domain-xxxxxxxx.json— failure metadata (timestamp, reason, recipients, subject) plus the original Microsoft GraphsendMailpayload (graph_payload) so an automated retry does not need to re-parse the .eml
Audit
typo3 microsoft-graph-mailer:list-undelivered
shows everything currently in the spool with the original failure reason and retry count.
Automatic retry (recommended)
typo3 microsoft-graph-mailer:resend-undelivered iterates the spool
and re-posts each entry's graph_payload to Microsoft Graph. On
success the pair is deleted; on failure the entry stays in the spool
with retry_count incremented and last_retry_error recorded.
Options:
--limit N— process at most N entries per run (default 50)--max-age-days D— delete (abandon) entries older than D days; 0 disables--dry-run— list what would be retried without contacting Graph--dir PATH— override the spool directory
Schedule this command in the TYPO3 Scheduler module as "Microsoft Graph: resend undelivered emails" (registered task) every five minutes. The task exposes a Batch size and Max age (days) field in the BE form.
Configuration
// Override the spool path: $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_graph_fallback_directory'] = '/srv/mail-spool/graph'; // Disable the fallback entirely (re-raises GraphMailerException, may break form-submits etc.): $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_graph_fallback_directory'] = false;
Multiple senders
The transport always targets the first active microsoft_graph OAuth
client. To send from a different mailbox in the same tenant, change the
sender_upn metadata value. To use a different tenant entirely, create
a second client row and deactivate the first.
Limitations
saveToSentItemsis hard-set tofalse— the service mailbox is typicallynoreply@…and shouldn't accumulate sent mail. If you need Sent-Folder copies, fork the transport.- Internet message headers are limited by Graph to
x--prefixed custom headers. Standard MIME headers (From/To/Subject/…) are mapped to the corresponding Graph message fields automatically. - Throttling: Microsoft Graph applies a 10,000 messages/24h limit per app per mailbox. For bulk newsletter traffic use a dedicated newsletter service (CleverReach, Mailchimp, …) instead.
License
GPL-2.0-or-later