Guide

Send-MgUserMail: Graph Email in PowerShell

Microsoft deprecated Send-MailMessage and pointed developers to the Graph PowerShell SDK. The replacement cmdlet, Send-MgUserMail, works but demands Azure AD app registration, Mail.Send permissions, and deeply nested hashtables. This guide walks through the full Graph workflow, then shows a 1-command alternative.

Written by Nick Barraclough Product Manager

VerifiedCLI 3.1.11 · Outlook · last tested May 21, 2026

Command references used in this guide: nylas email send for sending email and nylas auth config for API key authentication.

What is Send-MgUserMail and why does it exist?

Send-MgUserMail is a PowerShell cmdlet in the Microsoft.Graph.Users.Actions module that sends email through the Microsoft Graph API. Microsoft created it as the official successor to Send-MailMessage, which was deprecated in PowerShell 7.0 (March 2020) because its System.Net.Mail.SmtpClient can't negotiate TLS securely.

The deprecation warning is direct. Running Send-MailMessage in PowerShell 7 prints:

WARNING: The command 'Send-MailMessage' is obsolete.
This cmdlet does not guarantee secure connections to SMTP servers.

According to Microsoft's Send-MailMessage documentation, the recommended path is a third-party library. In practice, most Microsoft-centric teams land on the Graph PowerShell SDK. The SDK contains over 9,000 cmdlets across 40+ sub-modules, and the email-related ones live in Microsoft.Graph.Users.Actions and Microsoft.Graph.Mail.

Which Graph cmdlets handle email?

Microsoft Graph PowerShell splits email sending into 3 cmdlets, each wrapping a different Graph API endpoint. The split confuses teams migrating from Send-MailMessage, which was a single cmdlet that handled everything. Understanding which cmdlet to use depends on whether you're sending immediately, dispatching a saved draft, or creating a draft for later review.

CmdletGraph API endpointPurpose
Send-MgUserMailPOST /users/{id}/sendMailSend a new email immediately (most common)
Send-MgUserMessagePOST /users/{id}/messages/{id}/sendSend an existing draft (2-step: create then send)
New-MgUserMessagePOST /users/{id}/messagesCreate a draft without sending

For scripts that replace Send-MailMessage, Send-MgUserMail is the right choice. It sends in a single call, saves a copy to Sent Items by default, and doesn't require a separate draft step. The other 2 cmdlets are for draft workflows where a human reviews before sending.

How do I send with delegated auth (interactive login)?

Delegated authentication uses an interactive browser login where a user signs in and consents to the Mail.Send scope. This is the quickest path for ad-hoc scripts and developer workstations. The full flow takes 4 steps and about 25 lines of PowerShell, compared to 8 lines for the deprecated Send-MailMessage.

Start by installing the Graph module. The Microsoft.Graph.Users.Actions sub-module weighs approximately 15 MB and pulls in the core SDK as a dependency. Installation takes 30-90 seconds depending on your PowerShell gallery mirror.

# Step 1: Install the Graph module (one-time)
Install-Module Microsoft.Graph.Users.Actions -Scope CurrentUser -Force

Next, connect to Graph with the Mail.Send scope. This opens a browser window for authentication. The token is cached in memory for the duration of your PowerShell session, typically 60 minutes before refresh.

# Step 2: Connect with delegated permissions
Connect-MgGraph -Scopes "Mail.Send"

Build the message as a nested hashtable. The -BodyParameter format is the part that trips up most developers. Recipients must be wrapped in an EmailAddress object inside a ToRecipients array, and the body needs both ContentType and Content keys. Getting any level of nesting wrong produces a 400 Bad Request with a generic "Invalid request body" message.

# Step 3: Build the message params
$params = @{
    Message = @{
        Subject = "Quarterly report"
        Body = @{
            ContentType = "HTML"
            Content = "<h1>Q2 Results</h1><p>Revenue up 12% YoY.</p>"
        }
        ToRecipients = @(
            @{
                EmailAddress = @{
                    Address = "recipient@example.com"
                }
            }
        )
    }
}

# Step 4: Send
Send-MgUserMail -UserId "you@company.com" -BodyParameter $params

That's 20 lines of PowerShell for a single email. The nested hashtable format is required by the Graph API's JSON schema, and there's no shorthand parameter set on the cmdlet.

How do I send with app-only auth (unattended scripts)?

App-only authentication uses client credentials instead of an interactive login. This is the path for CI/CD pipelines, scheduled tasks, and background services that run without a user present. Setup requires 5 configuration steps in the Azure portal before writing any PowerShell, and the Azure AD app registration typically takes 10-15 minutes for someone familiar with the portal.

First, register an application in Azure AD. Navigate to portal.azure.com > Microsoft Entra ID > App registrations > New registration. Record the Application (client) ID and Directory (tenant) ID. Then create a client secret under Certificates & secrets. Secrets expire after a maximum of 24 months, so you'll need a rotation process.

Next, grant the Mail.Send application permission (not delegated) under API permissions > Microsoft Graph > Application permissions. An Entra ID admin must click "Grant admin consent" before the permission takes effect. Without this step, the cmdlet returns a 403 Forbidden error.

Once the Azure AD app is registered, connect using a certificate or client secret. The Connect-MgGraph documentation recommends certificate-based auth for production, but client secret is simpler for initial testing.

# Connect with client credentials (app-only)
$clientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$tenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$secret = Get-Secret -Name "GraphClientSecret" -AsPlainText

$secureSecret = ConvertTo-SecureString $secret -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($clientId, $secureSecret)

Connect-MgGraph -TenantId $tenantId -ClientSecretCredential $credential

After connecting, use the same Send-MgUserMail call from the delegated section. The -UserId parameter can be any mailbox in the tenant that the app permission covers, which is how shared mailboxes work with app-only auth.

How do I add attachments with Send-MgUserMail?

Attachments in Send-MgUserMail are base64-encoded byte arrays nested inside the Attachments property of the message hashtable. Each attachment needs 4 keys: @odata.type, Name, ContentType, and ContentBytes. The Graph API accepts up to 4 MB per attachment in a single request, or up to 150 MB using the upload session API.

The code below reads a local PDF file, encodes it to base64, and embeds it in the params hashtable. For files over 4 MB, you'd need to switch to the upload session endpoint, which adds another 15-20 lines of retry and chunk logic.

$fileBytes = [System.IO.File]::ReadAllBytes("C:Reportsinvoice.pdf")
$base64 = [System.Convert]::ToBase64String($fileBytes)

$params = @{
    Message = @{
        Subject = "Invoice attached"
        Body = @{
            ContentType = "Text"
            Content = "Please find the March invoice."
        }
        ToRecipients = @(
            @{ EmailAddress = @{ Address = "client@example.com" } }
        )
        Attachments = @(
            @{
                "@odata.type" = "#microsoft.graph.fileAttachment"
                Name = "invoice.pdf"
                ContentType = "application/pdf"
                ContentBytes = $base64
            }
        )
    }
}

Send-MgUserMail -UserId "you@company.com" -BodyParameter $params

That's 22 lines for a single attachment. Each additional file adds another nested hashtable entry inside the Attachments array.

How do I send from a shared mailbox?

Sending from a shared mailbox with Send-MgUserMail requires passing the shared mailbox address as the -UserId parameter. The calling user (delegated auth) or the Azure AD app (app-only auth) must have Mail.Send permission for that mailbox. In delegated mode, the user also needs "Send As" or "Send on Behalf" rights configured in Exchange Online, which an Exchange admin sets through the admin center or Set-Mailbox.

The Graph API doesn't differentiate between personal and shared mailboxes at the endpoint level. The same /users/{id}/sendMail endpoint handles both. The -UserId parameter accepts either a UPN (like support@company.com) or an object ID, and the Graph API routes the send through whichever mailbox matches.

# Send from a shared mailbox
$params = @{
    Message = @{
        Subject = "Support ticket #4821 resolved"
        Body = @{
            ContentType = "Text"
            Content = "Your issue has been resolved. Reply to reopen."
        }
        ToRecipients = @(
            @{ EmailAddress = @{ Address = "customer@example.com" } }
        )
    }
}

# Use the shared mailbox UPN as -UserId
Send-MgUserMail -UserId "support@company.com" -BodyParameter $params

The message appears in the shared mailbox's Sent Items folder. In organizations with more than 10 shared mailboxes, managing the "Send As" permissions across users and apps becomes its own maintenance burden.

How does the CLI compare to Send-MgUserMail?

Nylas CLI replaces the 25-line Graph PowerShell workflow with a single command. No Azure AD app registration, no Mail.Send permissions to configure, no nested hashtables. Authentication uses an API key stored once in your OS keychain, and the same command works across 6 providers. Setup takes under 2 minutes.

Install the CLI on Windows with the PowerShell install script. The script downloads the latest release from GitHub, verifies a SHA-256 checksum, and places the binary in ~/.config/nylas/bin. The entire download is under 20 MB.

# Install Nylas CLI on Windows
irm https://cli.nylas.com/install.ps1 | iex

For Homebrew, shell-script, and Go installs, see the getting started guide.

Authenticate once with your API key. The CLI stores it in Windows Credential Manager and reads it at runtime, so no secrets appear in your scripts. Authentication persists across sessions.

# One-time authentication
nylas auth config
# Paste your API key from dashboard-v3.nylas.com

# Verify the connection
nylas auth whoami

Now send the same email that took 25 lines with Send-MgUserMail. The CLI version is 4 lines including the backtick continuations. It handles OAuth2 token negotiation, TLS 1.2+ enforcement, and MIME type detection automatically.

nylas email send `
  --to "recipient@example.com" `
  --subject "Quarterly report" `
  --body "<h1>Q2 Results</h1><p>Revenue up 12% YoY.</p>" `
  --yes

The table below compares the two approaches across 8 dimensions. The Graph path offers deep Microsoft 365 integration (calendar, OneDrive, Teams), while the CLI prioritizes simplicity and provider portability.

DimensionSend-MgUserMailnylas email send
Lines to send 1 email25+4
Setup time10-15 min (Azure AD app)Under 2 min
AuthenticationMSAL / client credentialsAPI key in OS keychain
Providers supportedMicrosoft 365 onlyOutlook, Gmail, Exchange, Yahoo, iCloud, IMAP
Admin consent requiredYes (app-only)No
Attachment supportBase64 in hashtable (4 MB inline)Use Nylas API/SDK for MIME attachments
HTML detectionManual ContentType keyAuto-detected
Secret rotationClient secret expires in 24 monthsAPI key, no expiration

What are common Send-MgUserMail errors?

Graph PowerShell errors are notoriously vague. The API returns HTTP status codes with generic messages, and the PowerShell module wraps them in Microsoft.Graph.PowerShell.Authentication exceptions that hide the original error detail. These are the 5 most common failures when using Send-MgUserMail, based on Stack Overflow threads from 2023-2026.

403 Forbidden: insufficient permissions

The Azure AD app doesn't have Mail.Send permission, or admin consent hasn't been granted. Check API permissions in the Azure portal under App registrations > your app > API permissions. The "Status" column must show a green checkmark with "Granted for [tenant]".

400 Bad Request: invalid body

The hashtable structure doesn't match the Graph API schema. The most common mistake is putting Subject and Body at the top level of $params instead of nested under Message. Another frequent cause: using To instead of ToRecipients.

401 Unauthorized: token expired

The Connect-MgGraph session token expired (default: 60 minutes for delegated, 60 minutes for app-only). Re-run Connect-MgGraph to refresh. For long-running scripts, add a Connect-MgGraph call inside the loop or use the MSAL token cache.

ErrorSendAsDenied: mailbox permission missing

When sending from a shared mailbox, the user or app lacks "Send As" rights. An Exchange admin must grant it via Add-RecipientPermission -Identity shared@company.com -Trustee you@company.com -AccessRights SendAs.

ResourceNotFound: unknown UserId

The -UserId value doesn't match any mailbox in the tenant. Verify the UPN or object ID with Get-MgUser -UserId "user@company.com". Typos and deprovisioned accounts are the most common cause.

Next steps

After learning the Send-MgUserMail workflow, these guides cover related migration paths, comparisons, and PowerShell email patterns.