i all,
I want to document a fully working setup for sending email from SuiteCRM 8.9.x using Microsoft 365 via Microsoft Graph (SendMail).
This post focuses on what exactly must be configured in Microsoft and what must be changed in SuiteCRM.
No SMTP, no Basic Auth, no Authenticated SMTP.
This is a confirmed working solution.
Background / Problem Description
My situation:
-
SuiteCRM 8.9.x
-
Microsoft 365 / Exchange Online
-
Basic SMTP authentication is deprecated / disabled
-
Authenticated SMTP is not desired
-
OAuth tokens were refreshing correctly in SuiteCRM
-
Microsoft Graph access token was valid
-
BUT email sending still failed or silently fell back to SMTP
The root cause turned out to be inside SuiteCRM, not Microsoft:
PHPMailer returns recipient addresses as arrays, and this broke the Graph mail payload.
Once that was fixed, Graph SendMail worked immediately.
PART 1 β Microsoft 365 configuration (required)
All Microsoft-side configuration is done in Microsoft Entra ID
(formerly Azure Active Directory).
No Exchange Admin Center changes are required for Graph SendMail.
1. Create App Registration
Microsoft Entra ID β App registrations β New registration
-
Name:
SuiteCRM Graph Mail -
Supported account types:
- Single tenant
-
Redirect URI:
- Not required for SendMail
Save the application.
2. API permissions (CRITICAL)
App registration β API permissions
Add Microsoft Graph permissions:
Application permissions (NOT delegated)
Required:
Mail.Send
Optional (recommended for troubleshooting):
User.Read.All
Do NOT use:
-
Mail.Send.Shared -
Delegated permissions
3. Grant admin consent
Still in API permissions:
-
Click Grant admin consent
-
Confirm
If this step is skipped, Graph SendMail WILL fail.
4. Create client secret
App registration β Certificates & secrets β Client secrets
-
Create new secret
-
Copy the value immediately
-
Store it securely (used in SuiteCRM)
5. Token endpoint used by SuiteCRM
SuiteCRM uses OAuth 2.0 Client Credentials flow.
Token endpoint format:
https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token
Scopes (internally handled by SuiteCRM):
https://graph.microsoft.com/.default
6. Exchange / Security Defaults
Important clarifications:
-
Authenticated SMTP is NOT required -
Security Defaults do NOT need to be disabled -
No Exchange Admin Center settings are required -
No mailbox-level SMTP toggles are needed
Graph SendMail works independently of SMTP.
PART 2 β SuiteCRM 8.9.x configuration
This is where the actual bug was.
1. Create External OAuth Provider
Admin β External OAuth Providers
Create provider with:
-
Provider type:
Microsoft -
Token endpoint:
https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token -
Scope:
https://graph.microsoft.com/.default
2. Create External OAuth Connection
Admin β External OAuth Connections
Link it to the provider above.
Set:
-
Client ID β from Entra App Registration
-
Client Secret β from Entra App Registration
-
Grant type β Client Credentials
Save and verify:
-
access_tokenandrefresh_tokenappear in DB -
Token refresh works (SuiteCRM logs confirm this)
3. Configure Outbound Email in SuiteCRM
Admin β Email β Outbound Email Accounts
For System Outbound Email:
-
Mail send type:
SMTP -
Auth type:
OAuth -
External OAuth Connection: select the one created above
-
SMTP Host:
outlook.office365.com -
Port:
587 -
TLS enabled
Even though SMTP fields are filled, SMTP auth is never used.
SuiteCRM only uses this to decide routing β actual sending is done via Graph.
4. REQUIRED SuiteCRM code changes (core issue)
Root cause
PHPMailer methods return arrays:
$this->getToAddresses()
// returns:
[
['user@example.com', 'User Name'],
...
]
GraphMailer expects:
['user@example.com']
SuiteCRM core code treated these as strings β invalid Graph payload.
5. Fix in SugarPHPMailer.php
File:
public/legacy/include/SugarPHPMailer.php
Correct recipient extraction
$to = [];
foreach ((array)$this->getToAddresses() as $entry) {
if (is_array($entry) && !empty($entry[0])) {
$to[] = $entry[0];
}
}
$cc = [];
foreach ((array)$this->getCcAddresses() as $entry) {
if (is_array($entry) && !empty($entry[0])) {
$cc[] = $entry[0];
}
}
$bcc = [];
foreach ((array)$this->getBccAddresses() as $entry) {
if (is_array($entry) && !empty($entry[0])) {
$bcc[] = $entry[0];
}
}
These arrays are then passed to GraphMailer.
6. GraphMailer implementation
Location:
public/legacy/custom/include/GraphMail/GraphMailer.php
Responsibilities:
-
Accept already-clean email strings
-
Build Microsoft Graph payload:
/users/{from}/sendMail
-
Use
Authorization: Bearer <access_token> -
Log:
-
HTTP status
-
Response body (truncated)
-
Once recipient parsing was fixed, GraphMailer worked immediately.
Final Result
Emails send successfully via Microsoft 365
OAuth tokens refresh correctly
No SMTP authentication
No Security Defaults changes
Works for:
-
System email
-
User email
-
βSend Test Emailβ
Key lesson
This was NOT a Microsoft problem.
OAuth and Graph permissions were correct from the start.
The actual blocker was SuiteCRMβs handling of PHPMailer recipient arrays.
Recommendation to SuiteCRM core
SuiteCRM should:
-
Normalize PHPMailer recipients before using any non-SMTP transport
-
Document Graph SendMail as a first-class option
I reported this issue here: Microsoft 365 email via Graph API in SuiteCRM 8.9.x Β· Issue #10757 Β· SuiteCRM/SuiteCRM Β· GitHub
Hope this helps others save a lot of time ![]()