<< All versions

Skill v1.0.0

currentAutomated scan100/100
innoge/laravel-msgraph-mail/msgraph-mail
──Details
PublishedSeptember 29, 2026 at 05:54 PM
Content Hashsha256:2ce2f5aa02e17129...
Git SHA0d1ab4d7752c
──Files
Files (1 file, 8.0 KB)
SKILL.md8.0 KBactive
SKILL.md · 97 lines · 8.0 KB

version: "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-graph mailer.

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:

php
'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):

php
'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.ReadWrite application permission — otherwise a MissingMailReadWritePermission exception is thrown. save_to_sent_items is 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_in lifetime minus 60s. Tokens do not pick up credential or permission changes until refreshed — run php artisan cache:clear after 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

SymptomCauseFix
Configuration key tenant_id/client_id/client_secret/from.address for microsoft-graph mailer is missingMailer entry missing from config/mail.php (common in apps upgraded from older Laravel versions), empty env vars, or stale config cacheAdd the full mailer block above, set the env vars, run php artisan config:clear
AADSTS7000215: Invalid client secretWrong or expired client secret, or the secret's ID was copied instead of its ValueCreate a new secret, copy the Value, update env; check secret expiry — or switch to certificate auth
AADSTS700016: Application ... was not foundWrong client_id or app registered in a different tenantVerify Application (client) ID and Directory (tenant) ID on the app's Overview page
AADSTS90002: Tenant ... not foundWrong tenant_idUse the Directory (tenant) ID GUID
AADSTS700027: Client assertion contains an invalid signatureCertificate not uploaded to the app registration, or wrong certificate/private key pairUpload the certificate under Certificates & secrets → Certificates; verify thumbprints match
403 ErrorAccessDeniedMail.Send application permission missing, admin consent not granted, or an Exchange ApplicationAccessPolicy blocks this mailboxAdd Mail.Send as an application permission and grant admin consent; check access policies
MissingMailReadWritePermission exceptionMail exceeds ~3 MB and the app lacks Mail.ReadWriteGrant 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 persistThe cached token was issued before the grant and does not contain the new rolephp artisan cache:clear
404 ErrorInvalidUser: The requested user '...' is invalidThe from address is not an existing mailbox, or is an alias instead of the primary SMTP addressUse the mailbox's primary SMTP address; verify the mailbox exists in the tenant
Auth keeps failing after rotating the client secretOld token still cachedphp artisan cache:clear
ErrorMessageSizeExceededAttachments 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 timeoutsTransient Graph API or DNS issuesQueue mails with retries and backoff; don't raise timeouts expecting a fix
Mail arrives but is not in Sent Itemssave_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.

All versions