Send Emails via Microsoft Graph API Using PowerShell and App Registration
Microsoft Graph API is the unified gateway to Microsoft 365 data and services. One of the most common automation scenarios is sending emails programmatically — without relying on a logged-in user, Outlook, or SMTP. In this post, we'll walk through how to register an Azure AD app, grant it the right permissions, and use a single end-to-end PowerShell script to send emails and query SharePoint via Graph API using client credentials (app-only auth).
Why Use Graph API for Sending Emails?
- No user sign-in required — works great for background jobs, bots, and automation flows
- Works from anywhere — PowerShell scripts, Power Automate Desktop, Azure Functions, etc.
- Scalable and auditable — full control over sending identity and logging
Step 1: Register an App in Microsoft Entra ID (Azure AD)
Before writing any code, you need an app registration that represents your script or automation.
- Go to https://portal.azure.com
- Navigate to Microsoft Entra ID → App registrations → New registration
- Give it a meaningful name (e.g.,
GraphAPI-MailSender) - Leave Redirect URI blank — not needed for client credentials flow
- Click Register
Once created, note down:
- Application (client) ID → used as
$clientIdin the script - Directory (tenant) ID → used in the token endpoint URL
Step 2: Create a Client Secret
- Go to Certificates & secrets → New client secret
- Add a description and set an expiry (e.g., 12 months)
- Copy the Value immediately — it won't be shown again
- Store it securely (Azure Key Vault is recommended for production)
Step 3: Grant API Permissions
Your app needs the following Application permissions (not Delegated):
| Permission | Purpose |
|---|---|
Mail.Send |
Send email as any mailbox in the tenant |
Sites.Read.All |
Read SharePoint site data |
Steps:
- Go to API permissions → Add a permission → Microsoft Graph → Application permissions
- Add
Mail.SendandSites.Read.All - Click Grant admin consent — required for application permissions
⚠️
Mail.Sendas an application permission allows sending mail as any user in the tenant. Use application access policies to restrict it to specific mailboxes in production.
Step 4: Full PowerShell Script
The script below handles everything in sequence:
- Acquires a token using client credentials
- Sends an email via Graph API
- Reuses the token to query the SharePoint root site
# ============================================================
# Microsoft Graph API — Send Email + Query SharePoint Root Site
# Using App-Only (Client Credentials) Authentication
# ============================================================
# ── Configuration ──────────────────────────────────────────
$clientId = "<YOUR_CLIENT_ID>" # Application (client) ID
$clientSecret = "<YOUR_CLIENT_SECRET>" # Client secret value
$tenantId = "<YOUR_TENANT_ID>" # Directory (tenant) ID
$senderUPN = "sender@yourdomain.com" # Mailbox used to send email
$recipientUPN = "recipient@yourdomain.com" # Target recipient
# ── Part 1: Acquire Access Token ────────────────────────────
Write-Output "Acquiring access token..."
$tokenBody = @{
client_id = $clientId
client_secret = $clientSecret
scope = "https://graph.microsoft.com/.default"
grant_type = "client_credentials"
}
$tokenResponse = Invoke-RestMethod `
-Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" `
-Method POST `
-Body $tokenBody
$token = $tokenResponse.access_token
Write-Output "Token acquired successfully."
# ── Part 2: Send Email via Graph API ────────────────────────
Write-Output "Sending email..."
$mailPayload = @{
message = @{
subject = "Test Email from Graph API"
body = @{
contentType = "Text"
content = "This is a test email sent via Microsoft Graph API using PowerShell with app-only authentication."
}
toRecipients = @(
@{
emailAddress = @{
address = $recipientUPN
}
}
)
}
} | ConvertTo-Json -Depth 10
$headers = @{
Authorization = "Bearer $token"
"Content-Type" = "application/json"
}
try {
Invoke-RestMethod `
-Uri "https://graph.microsoft.com/v1.0/users/$senderUPN/sendMail" `
-Method POST `
-Headers $headers `
-Body $mailPayload `
-ContentType "application/json"
Write-Output "✅ Email sent successfully to $recipientUPN"
}
catch {
Write-Error "❌ Failed to send email: $_"
}
# ── Part 3: Query SharePoint Root Site ──────────────────────
Write-Output "`nQuerying SharePoint root site..."
try {
$rootSite = Invoke-RestMethod `
-Uri "https://graph.microsoft.com/v1.0/sites/root" `
-Method GET `
-Headers $headers
Write-Output "✅ SharePoint root site retrieved:"
Write-Output ($rootSite | ConvertTo-Json -Depth 3)
}
catch {
Write-Error "❌ Failed to query SharePoint root site: $_"
}
How to Run the Script
- Open PowerShell (or Windows PowerShell ISE / VS Code)
- Replace the four placeholder values at the top of the script:
<YOUR_CLIENT_ID><YOUR_CLIENT_SECRET><YOUR_TENANT_ID>sender@yourdomain.comandrecipient@yourdomain.com
- Save as
Send-GraphEmail.ps1 - Run:
.\Send-GraphEmail.ps1
Expected Output
Acquiring access token...
Token acquired successfully.
Sending email...
✅ Email sent successfully to recipient@yourdomain.com
Querying SharePoint root site...
✅ SharePoint root site retrieved:
{
"id": "yourtenant.sharepoint.com,xxxxxxxx-...",
"displayName": "Communication site",
"name": "root",
"webUrl": "https://yourtenant.sharepoint.com"
}
Script Breakdown
| Section | What it does |
|---|---|
| Configuration block | Centralises all credentials and addresses at the top — easy to maintain |
| Token acquisition | Posts to the OAuth 2.0 token endpoint using client credentials |
| Email sending | Constructs the mail JSON payload and calls /users/{sender}/sendMail |
| SharePoint query | Reuses the same token to call /sites/root — no second token needed |
| try/catch blocks | Catches errors per operation so one failure doesn't stop the rest |
Security Best Practices
| Practice | Recommendation |
|---|---|
| Never hardcode secrets | Move $clientSecret to Azure Key Vault or environment variables |
| Rotate secrets regularly | Set reminders before expiry; automate via Key Vault rotation |
Restrict Mail.Send scope |
Use application access policies to limit to specific mailboxes |
| Prefer certificates | Certificate-based auth (New-SelfSignedCertificate) is more secure than secrets |
| Audit permissions | Review app registrations and permissions in Entra ID quarterly |
Summary
In this post, we covered:
- Registering an app in Microsoft Entra ID and creating a client secret
- Granting
Mail.SendandSites.Read.Allapplication permissions with admin consent - A complete PowerShell script that acquires a token, sends an email, and queries SharePoint — all using app-only auth
- Error handling with
try/catchper operation - Security recommendations for production use
This pattern is perfect for Power Automate Desktop flows, scheduled PowerShell jobs, or any scenario where no interactive user is present. The same token works across multiple Graph API endpoints — making it efficient to chain operations in a single script.
Tags: MicrosoftGraph, PowerShell, AzureAD, AppRegistration, Microsoft365, EmailAutomation, GraphAPI, EntraID, PowerAutomate, SharePoint
Part 2: End-to-End in Power Automate Desktop (PAD)
The same logic — get a token, send an email, query SharePoint — can run entirely inside a Power Automate Desktop flow without any external PowerShell script. PAD has built-in HTTP actions and a Run PowerShell script action, giving you two clean approaches.
Approach A: Using the "Run PowerShell Script" Action (Quickest Way)
This embeds the entire PowerShell script directly inside a PAD flow. Best when you already have the script and just want to automate it.
PAD Flow Structure
Flow: Send Email via Graph API
│
├── [1] Set Variable — clientId
├── [2] Set Variable — clientSecret
├── [3] Set Variable — tenantId
├── [4] Set Variable — senderUPN
├── [5] Set Variable — recipientUPN
├── [6] Run PowerShell Script
│ └── Script: (full script using %clientId%, %clientSecret%, etc.)
│ └── Output: PowershellOutput
├── [7] IF PowershellOutput contains "successfully"
│ └── Display Message — "Flow completed successfully"
│ ELSE
│ └── Display Message — "Flow encountered an error"
└── [8] (Optional) Write output to text file or log
Step-by-Step in PAD Designer
Step 1 — Set your variables
Add one Set variable action for each credential. Using variables (rather than hardcoding) keeps the script clean and makes future updates easy.
| Variable Name | Value |
|---|---|
clientId |
Your Application (client) ID |
clientSecret |
Your client secret value |
tenantId |
Your Directory (tenant) ID |
senderUPN |
sender@yourdomain.com |
recipientUPN |
recipient@yourdomain.com |
In PAD, go to Actions panel → Variables → Set variable, set Name and Value for each.
Step 2 — Add "Run PowerShell Script" action
Search for Run PowerShell script in the Actions panel (under Scripting). Paste the script below into the script body. PAD variables are referenced using %variableName% syntax inside the script block.
# ============================================================
# Graph API — Send Email + Query SharePoint Root Site
# Running inside Power Automate Desktop
# ============================================================
$clientId = "%clientId%"
$clientSecret = "%clientSecret%"
$tenantId = "%tenantId%"
$senderUPN = "%senderUPN%"
$recipientUPN = "%recipientUPN%"
# ── Part 1: Acquire Token ────────────────────────────────────
$tokenBody = @{
client_id = $clientId
client_secret = $clientSecret
scope = "https://graph.microsoft.com/.default"
grant_type = "client_credentials"
}
$tokenResponse = Invoke-RestMethod `
-Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" `
-Method POST `
-Body $tokenBody
$token = $tokenResponse.access_token
# ── Part 2: Send Email ───────────────────────────────────────
$mailPayload = @{
message = @{
subject = "Test Email from Graph API via PAD"
body = @{
contentType = "Text"
content = "This email was sent via Microsoft Graph API running inside a Power Automate Desktop flow."
}
toRecipients = @(
@{ emailAddress = @{ address = $recipientUPN } }
)
}
} | ConvertTo-Json -Depth 10
$headers = @{
Authorization = "Bearer $token"
"Content-Type" = "application/json"
}
try {
Invoke-RestMethod `
-Uri "https://graph.microsoft.com/v1.0/users/$senderUPN/sendMail" `
-Method POST `
-Headers $headers `
-Body $mailPayload `
-ContentType "application/json"
Write-Output "Email sent successfully to $recipientUPN"
}
catch {
Write-Output "ERROR sending email: $_"
}
# ── Part 3: Query SharePoint Root Site ──────────────────────
try {
$rootSite = Invoke-RestMethod `
-Uri "https://graph.microsoft.com/v1.0/sites/root" `
-Method GET `
-Headers $headers
Write-Output "SharePoint root site: $($rootSite.webUrl)"
}
catch {
Write-Output "ERROR querying SharePoint: $_"
}
In the action settings:
- Script to run → paste the script above
- PowerShell script output → save to variable
PowershellOutput - Script error output → save to variable
ScriptError
Step 3 — Handle output with an IF condition
Add an If action:
- First operand:
%PowershellOutput% - Operator: Contains
- Second operand:
successfully
Inside the If block → add Display message: "✅ Email sent and SharePoint queried successfully"
Inside the Else block → add Display message: "❌ Flow error — check ScriptError variable"
Step 4 — (Optional) Log output to a file
Add a Write text to file action after the If block:
- File path:
C:\PAD-Logs\GraphAPI-Run-%CurrentDateTime%.txt - Text to write:
%PowershellOutput%%NewLine%%ScriptError% - If file exists: Append
Approach B: Using PAD HTTP Actions (No PowerShell Required)
PAD has native HTTP actions under Web → Invoke web service that can call REST APIs directly — no PowerShell needed. This is the cleanest approach for PAD-native flows.
PAD Flow Structure
Flow: Send Email via Graph API (HTTP Native)
│
├── [1] Set Variable — clientId
├── [2] Set Variable — clientSecret
├── [3] Set Variable — tenantId
├── [4] Set Variable — senderUPN
├── [5] Set Variable — recipientUPN
│
├── [6] Invoke web service — POST token endpoint
│ └── Saves response → TokenResponse (JSON)
│
├── [7] Get JSON key — extract access_token
│ └── Saves → AccessToken
│
├── [8] Invoke web service — POST sendMail
│ └── Uses Bearer %AccessToken% in header
│ └── Saves response → MailResult
│
├── [9] Invoke web service — GET sites/root
│ └── Uses Bearer %AccessToken% in header
│ └── Saves response → SharePointResult
│
└── [10] Display message — show results
Step-by-Step: HTTP Actions in PAD
Step 1 — Set variables (same as Approach A)
Step 2 — Get Access Token (Invoke web service)
Action: Web → Invoke web service
| Field | Value |
|---|---|
| URL | https://login.microsoftonline.com/%tenantId%/oauth2/v2.0/token |
| Method | POST |
| Accept | application/json |
| Content type | application/x-www-form-urlencoded |
| Custom headers | (leave blank) |
| Request body | client_id=%clientId%&client_secret=%clientSecret%&scope=https://graph.microsoft.com/.default&grant_type=client_credentials |
| Save response into | TokenResponse |
Step 3 — Extract the access token
Action: Variables → Get JSON key value
(or use Convert JSON to custom object then dot-notation)
- JSON:
%TokenResponse% - Key:
access_token - Save into:
AccessToken
Alternatively, use a Run PowerShell script just for this one-liner:
$json = '%TokenResponse%' | ConvertFrom-Json
Write-Output $json.access_token
Save output into AccessToken.
Step 4 — Send Email (Invoke web service)
Action: Web → Invoke web service
| Field | Value |
|---|---|
| URL | https://graph.microsoft.com/v1.0/users/%senderUPN%/sendMail |
| Method | POST |
| Accept | application/json |
| Content type | application/json |
| Custom headers | Authorization: Bearer %AccessToken% |
| Request body | (see JSON below) |
| Save response into | MailResult |
Request body (paste as-is, update addresses via variables):
{
"message": {
"subject": "Test Email from PAD via Graph API",
"body": {
"contentType": "Text",
"content": "This email was sent using Power Automate Desktop with Microsoft Graph API HTTP actions."
},
"toRecipients": [
{
"emailAddress": {
"address": "%recipientUPN%"
}
}
]
}
}
Step 5 — Query SharePoint Root Site (Invoke web service)
Action: Web → Invoke web service
| Field | Value |
|---|---|
| URL | https://graph.microsoft.com/v1.0/sites/root |
| Method | GET |
| Accept | application/json |
| Custom headers | Authorization: Bearer %AccessToken% |
| Save response into | SharePointResult |
Step 6 — Display results
Action: Message boxes → Display message
Message: SharePoint site: %SharePointResult%
Approach Comparison
| Approach A (Run PowerShell) | Approach B (HTTP Actions) | |
|---|---|---|
| Complexity | Low — paste and run | Medium — multiple actions |
| PAD native | No (calls PowerShell) | Yes (pure PAD) |
| Debugging | Via script output variable | Per-action response variables |
| Best for | Existing PowerShell scripts | New PAD-native flows |
| Token parsing | Built into the script | Needs extra step in PAD |
| Error handling | try/catch in script | On block error / IF conditions |
Recommendation: Use Approach A if you already have the PowerShell script. Use Approach B if you want a fully PAD-native flow with no external dependencies.
Tips for PAD Flows with Graph API
- Store secrets in PAD Input variables marked as Sensitive — they are masked in logs and not stored in plain text in the flow definition
- Add On block error handlers around each Invoke web service action to gracefully catch HTTP failures (401 Unauthorized, 403 Forbidden, etc.)
- Token expiry: The client credentials token is valid for 1 hour. For long-running flows, re-acquire the token mid-flow if needed
- Test in PAD debugger using the step-through (F10) to inspect each variable value before running end-to-end
Full Flow Summary
App Registration (Entra ID)
│
▼
Client Credentials → OAuth 2.0 Token Endpoint
│
▼
Access Token (Bearer)
│
├──▶ POST /users/{sender}/sendMail → Email delivered ✅
│
└──▶ GET /sites/root → SharePoint data ✅
All three entry points — standalone PowerShell, PAD via PowerShell action, and PAD via HTTP actions — use the exact same app registration, permissions, and token. The only difference is where and how the script runs.
Tags: MicrosoftGraph, PowerShell, PowerAutomateDesktop, PAD, AzureAD, AppRegistration, Microsoft365, EmailAutomation, GraphAPI, EntraID, SharePoint, RPA
No comments:
Post a Comment