Send Email in SharePoint with SharePoint REST API and PnPjs : What You Need to Know

Important update: the API this post was originally about — SP.Utilities.Utility.SendEmail — was retired by Microsoft on October 31, 2025. If you have code using the raw REST or PnPjs examples further down, it has stopped working. The current, supported way to send email programmatically from SharePoint is Microsoft Graph’s sendMail API. This post now leads with that.


In this post: The current way: Microsoft Graph sendMail · Send-PnPMail: the PowerShell path, with attachments · The retired approach (for migration reference only) · The actual sending limits · When would you actually use this? · Best practices · Related reading


The current way: Microsoft Graph sendMail

Graph’s sendMail endpoint is the direct, supported replacement — and it’s a genuine upgrade, not just a forced migration: it supports real file attachments and sending to distribution lists, both of which the old SharePoint API never could. The tradeoff is real setup: this needs an Azure AD app registration with the Mail.Send permission (delegated for “send as the signed-in user,” application-level with admin consent if the calling code needs to send as any user in the tenant), where the old API just needed a request digest.

POST https://graph.microsoft.com/v1.0/me/sendMail
Authorization: Bearer {access_token}
Content-Type: application/json

{
  "message": {
    "subject": "Custom Notification",
    "body": {
      "contentType": "Text",
      "content": "Hello! This is a custom email sent via Microsoft Graph."
    },
    "toRecipients": [
      { "emailAddress": { "address": "user@domain.com" } }
    ]
  }
}

For app-only (application permission) scenarios — a background job or automation with no signed-in user — call POST /users/{id}/sendMail instead of /me/sendMail, with a token acquired via client credentials flow and the Mail.Send application permission granted with admin consent.


Send-PnPMail: the PowerShell path, with attachments

For scripts and admin automation, PnP PowerShell’s Send-PnPMail wraps the same Graph endpoint and is the simplest path — and unlike anything the old SharePoint REST API could do, it supports both local file attachments and files already sitting in the SharePoint site:

Connect-PnPOnline -Url "https://yourtenant.sharepoint.com/sites/yoursite" -Interactive

# Basic send
Send-PnPMail -From "user@contoso.onmicrosoft.com" -To "recipient@contoso.com" `
    -Subject "Custom Notification" -Body "Hello from PnP PowerShell."

# With a local file and a SharePoint-hosted file attached
Send-PnPMail -From "user@contoso.onmicrosoft.com" -To "recipient@contoso.com" `
    -Subject "Report attached" -Body "See attached." `
    -Attachments "C:\Reports\summary.pdf" `
    -Files "/sites/yoursite/Shared Documents/Q3-Report.docx"

This is genuinely the best option for the scenarios the old API’s limitations used to rule out entirely — anything needing an attachment, or a scheduled/admin script rather than interactive client-side code.


The retired approach (for migration reference only)

Kept below so that anyone who inherits old code using this pattern can recognize it and knows what to replace it with — none of the following still works. This was the raw REST call to the now-retired SP.Utilities.Utility.SendEmail:

// RETIRED -- SP.Utilities.Utility.SendEmail no longer works as of Oct 31, 2025
function sendEmail() {
    var siteUrl = _spPageContextInfo.webAbsoluteUrl;

    $.ajax({
        contentType: 'application/json',
        url: siteUrl + "/_api/SP.Utilities.Utility.SendEmail",
        type: "POST",
        data: JSON.stringify({
            'properties': {
                '__metadata': { 'type': 'SP.Utilities.EmailProperties' },
                'To': { 'results': ['user@domain.com'] },
                'Subject': 'Custom Notification',
                'Body': 'Hello! This is a custom email from SharePoint.'
            }
        }),
        headers: {
            "Accept": "application/json;odata=verbose",
            "X-RequestDigest": $("#__REQUESTDIGEST").val()
        }
    });
}

And the equivalent old PnPjs call (also using the outdated v2 import { sp } from "@pnp/sp/presets/all" pattern on top of the retired endpoint underneath it):

// RETIRED -- both the API call and the PnPjs v2 import pattern are outdated
import { sp } from "@pnp/sp/presets/all";

sp.utility.sendEmail({
    To: ["user@domain.com"],
    Subject: "PnPjs Email",
    Body: "This email was sent using PnPjs!",
});

If this is what your existing code looks like, the fix is switching to Graph’s sendMail or Send-PnPMail above — not a syntax tweak, a genuine endpoint change with new permissions to set up.


The actual sending limits

“Don’t fire these inside a tight loop” in the Best Practices below is easy to agree with and vague about what the actual ceiling is — worth naming the real numbers, since they’re what determines whether a design needs batching/queueing at all. Exchange Online mailboxes are capped at 10,000 recipients per day and roughly 30 messages per minute — a single message to 50 people counts as 50 against the daily figure, not 1, so a notification going to a large distribution list burns through that limit faster than the message count alone suggests. Some smaller-tier business plans see a tighter daily cap (as low as 5,000), so it’s worth confirming the specific limit for the sending account’s actual license rather than assuming the enterprise number applies everywhere.

Separate from the mailbox-level send limit is Graph’s own API-request throttling — 10,000 requests per 10 minutes, per user, per app — which matters specifically for an app-only automation calling sendMail in a loop rather than a human sending mail normally. Hit either ceiling and Graph returns a 429 with a Retry-After header; the fix is the same pattern as any other Graph throttling response — back off for the duration given, don’t just retry immediately — not a special case unique to mail sending.


SP.Utilities.Utility.SendEmail was retired October 31, 2025. If you inherited code using it, the fix is Microsoft Graph’s sendMail — a real endpoint change, not a syntax update.

When would you actually use this?
  • A list item hits a certain status and someone needs a notification — without spinning up a full Power Automate flow for something this small.
  • You’re building a custom SPFx web part and need to send a confirmation email, now with the option to actually attach a file.
  • You inherited old code calling SP.Utilities.Utility.SendEmail and need to know what broke and why — see the retired section above.
  • You need email sent from an admin script on a schedule — Send-PnPMail, not client-side code at all.

Best practices
  • Audit for the retired API first if you’re maintaining an older SharePoint solution — Purview audit logs can help identify where SP.Utilities.Utility.SendEmail is still being called from code that predates the retirement.
  • Request only the Mail.Send permission scope needed for the use case — delegated if a signed-in user is sending, application-level (with admin consent) only if the code genuinely needs to send as arbitrary users.
  • Validate email addresses before sending — Graph won’t do this for you either.
  • Sanitize any user input going into the HTML body to avoid injection.
  • Wrap calls in proper error handling — don’t let a failed send disappear silently.
  • Don’t fire these inside a tight loop without batching — Graph has its own throttling limits.


That’s the fix — let me know in the comments if it doesn’t quite match your setup.

App Catalog Authentication Automation Backup Compliance Content Type CSS Flows Google Javascript Limitations List Metadata MFA Microsoft Node NodeJs O365 OneDrive Permissions PnP PnPJS Policy PowerApps Power Automate PowerAutomate PowerPlatform PowerShell React ReactJs Rest API Rest Endpoint Security Send an HTTP Request to SharePoint SharePoint SharePoint List SharePoint Modern SharePoint Online SPFX SPO Sync Tags Teams Termstore Versioning

Leave a Comment

Your email address will not be published. Required fields are marked *