Skill v1.0.0
currentAutomated scan100/100version: "1.0.0" name: msgraph-mail description: Configure, use, and troubleshoot the innoge/laravel-msgraph-mail package, which sends Laravel mail through the Microsoft Graph API instead of SMTP. Use when setting up the microsoft-graph mailer, registering the Azure app for it, configuring certificate authentication, or debugging Graph mail sending errors (401/403/404, AADSTS errors, timeouts). license: MIT metadata: author: innoge
Microsoft Graph Mail
When to use this skill
Use this skill when working with the innoge/laravel-msgraph-mail package (2.x):
- Configuring Laravel to send mail through Microsoft 365 / Microsoft Graph (replacing deprecated Office 365 SMTP).
- Creating or fixing the required Azure app registration, including certificate authentication.
- Debugging failed sends through the
microsoft-graphmailer.
Configuration
The package registers a microsoft-graph mail transport via package auto-discovery. There is no publishable package config — all configuration lives in config/mail.php under mailers:
'mailers' => ['microsoft-graph' => ['transport' => 'microsoft-graph','client_id' => env('MICROSOFT_GRAPH_CLIENT_ID'),'client_secret' => env('MICROSOFT_GRAPH_CLIENT_SECRET'),'tenant_id' => env('MICROSOFT_GRAPH_TENANT_ID'),'from' => [ // optional: omit the key entirely to use the global mail.from'address' => env('MAIL_FROM_ADDRESS'),'name' => env('MAIL_FROM_NAME'),],'save_to_sent_items' => env('MAIL_SAVE_TO_SENT_ITEMS', false), // optional],// ...],
Then activate it with MAIL_MAILER=microsoft-graph plus the MICROSOFT_GRAPH_* env vars.
tenant_id and client_id are always required. For authentication provide EITHER client_secret OR a client_certificate block (certificate wins when both are present):
'client_certificate' => ['certificate' => env('MICROSOFT_GRAPH_CERTIFICATE'), // PEM content or file path'private_key' => env('MICROSOFT_GRAPH_PRIVATE_KEY'), // PEM content or file path'passphrase' => env('MICROSOFT_GRAPH_KEY_PASSPHRASE'), // optional],
Certificate auth signs a JWT client assertion (requires ext-openssl); the certificate must be uploaded to the app registration under Certificates & secrets → Certificates. It avoids expiring client secrets.
The from address (mailer-level or global mail.from) must be the primary SMTP address of a real mailbox (licensed user or shared mailbox) in the tenant — Graph sends as that mailbox.
Azure prerequisite
The package uses the OAuth2 client-credentials flow, so it needs an Entra ID (Azure AD) app registration with the Microsoft Graph `Mail.Send` application permission (not delegated) and admin consent granted. Sending mails larger than ~3 MB additionally requires `Mail.ReadWrite`.
Read references/azure-app-registration.md when the user needs to create the app registration, obtain the tenant/client ID or credentials, or fix permission/consent problems.
How it works and behavior notes
- Sending is a
POST https://graph.microsoft.com/v1.0/users/{from-address}/sendMail. Overriding the sender per mailable with->from('other@tenant.com')changes the URL target — that address must also be a real mailbox the app may send as. - Mails work with standard
Mail::, Mailables, and Notifications; no package-specific API. - Large mails (total payload over ~3 MB): automatically sent via a draft message + chunked attachment upload sessions. Requires the
Mail.ReadWriteapplication permission — otherwise aMissingMailReadWritePermissionexception is thrown.save_to_sent_itemsis honored: when disabled, the sent message is deleted from Sent Items afterwards; failed sends clean up their draft. - `save_to_sent_items` is read from the mailer's own config entry (works with mailers under any key). Per-mailable override:
$mailable->withSymfonyMessage(fn ($m) => $m->getHeaders()->add(new \Symfony\Component\Mailer\Header\MetadataHeader('save-to-sent-items', 'true'))). - The OAuth token is cached per tenant + client for its
expires_inlifetime minus 60s. Tokens do not pick up credential or permission changes until refreshed — runphp artisan cache:clearafter rotating secrets/certificates or granting new Graph permissions. - Attachment names use the real filename with extension where the framework exposes one; inline-image content ids are separate and stable.
- Only custom headers whose name starts with
X-are forwarded to Graph (Symfony metadata/tag headers excluded); other custom headers are silently dropped. Recipient display names are dropped (only addresses are sent — Microsoft substitutes tenant-known names). - The Graph API can be transiently slow or unreachable; queue mail with retries (
$tries, backoff) rather than sending synchronously in requests.
Troubleshooting
| Symptom | Cause | Fix | |
|---|---|---|---|
Configuration key tenant_id/client_id/client_secret/from.address for microsoft-graph mailer is missing | Mailer entry missing from config/mail.php (common in apps upgraded from older Laravel versions), empty env vars, or stale config cache | Add the full mailer block above, set the env vars, run php artisan config:clear | |
AADSTS7000215: Invalid client secret | Wrong or expired client secret, or the secret's ID was copied instead of its Value | Create a new secret, copy the Value, update env; check secret expiry — or switch to certificate auth | |
AADSTS700016: Application ... was not found | Wrong client_id or app registered in a different tenant | Verify Application (client) ID and Directory (tenant) ID on the app's Overview page | |
AADSTS90002: Tenant ... not found | Wrong tenant_id | Use the Directory (tenant) ID GUID | |
AADSTS700027: Client assertion contains an invalid signature | Certificate not uploaded to the app registration, or wrong certificate/private key pair | Upload the certificate under Certificates & secrets → Certificates; verify thumbprints match | |
403 ErrorAccessDenied | Mail.Send application permission missing, admin consent not granted, or an Exchange ApplicationAccessPolicy blocks this mailbox | Add Mail.Send as an application permission and grant admin consent; check access policies | |
MissingMailReadWritePermission exception | Mail exceeds ~3 MB and the app lacks Mail.ReadWrite | Grant Mail.ReadWrite application permission + admin consent, then php artisan cache:clear (cached tokens lack new permissions) — or reduce attachment size | |
| Permission was granted but errors persist | The cached token was issued before the grant and does not contain the new role | php artisan cache:clear | |
404 ErrorInvalidUser: The requested user '...' is invalid | The from address is not an existing mailbox, or is an alias instead of the primary SMTP address | Use the mailbox's primary SMTP address; verify the mailbox exists in the tenant | |
| Auth keeps failing after rotating the client secret | Old token still cached | php artisan cache:clear | |
ErrorMessageSizeExceeded | Attachments exceed Graph's hard 150 MB upload-session limit (mails up to ~150 MB are handled automatically) | Send download links instead of attachments | |
cURL error 28 / connection timeouts | Transient Graph API or DNS issues | Queue mails with retries and backoff; don't raise timeouts expecting a fix | |
| Mail arrives but is not in Sent Items | save_to_sent_items is false (default) | Enable it in the mailer config or per mailable via the metadata header |
For live debugging, use Boost's MCP tools where available: last-error / read-log-entries to see the actual Graph error response, and search-docs for Laravel mail documentation.
Version note
Package 1.x behaves differently in three relevant ways: save_to_sent_items only works on the mailer key literally named microsoft-graph, large mails fail with ErrorMessageSizeExceeded (~4 MB cap, no upload sessions), and there is no certificate auth. See UPGRADE.md in the package for the full 1.x → 2.0 migration guide.