PowerShell: Sending Emails with Graph
Posted on: April 12, 2026
If you do a lot of automating, you'll likely find a need to have reporting or errors sent from within your scripts to an email address or distribution list. And if you're a numbers nerd like me, you'll really appreciate receiving periodic emails with license availability and usage statistics, app secrets that are coming up on expiration, risk trends in your Entra ID tenant, etc. For this, we'll be diving into Send-MgUserMail, the modern way to send emails with Microsoft's Graph API.
Send-MgUserMail
If you've been scripting with PowerShell for a while, you may be familiar with the Send-MailMessage cmdlet. It facilitates connecting to an SMTP server and sending an email from a mailbox located there. You may also be aware that this cmdlet was officially declared to be obsolete around 2020. It doesn't guarantee a secure connection to an SMTP server and it relies on the deprecated .NET class called SmtpClient. Microsoft recommends against using it.
So, Send-MailMessage is no longer the best way to send emails from a script. Where does that leave us? Send-MgUserMail. Send-MgUserMail is not exactly a replacement, but it is what Microsoft refers to as the "modern solution" to sending emails programmatically with PowerShell. This cmdlet lives in the Microsoft.Graph.User.Actions module and requires connection to Graph.
As far as API permissions are concerned, Mail.Send will allow you to send emails as a user, and Mail.Send.Shared lets you send emails on behalf of someone else, a mailbox you have delegated permissions to, for example. Personally, Mail.Send has worked for every use case I have had. If we're applying these permissions to an app registration and using it to send mail, the delegated permission allows sending email from mailboxes that the authenticated user has permission to send from. The application permission allows sending email from any mailbox in the tenant. Grant and consent to these wisely.
Caveat
Before we get into setup and examples, there is a major caveat that I've come across. If you are using this cmdlet in a user-initiated script, the authenticated user must have a mailbox if delegated permissions are in use. This is regardless of whether you are sending the email from your own mailbox or from a shared mailbox. This is not an ideal requirement if you are running this script using something like a dedicated admin account, which ideally shouldn't have a mailbox per best practices. Not having a mailbox results in a rather vague error. With that in mind, lets continue.
Setup
Send-MgUserMail only has a few requirements. That said, one of those requirements is a request body, containing multiple parameters. -UserId specifies the authenticated user sending the email. This can be a user's object ID, but it also takes UserPrincipalName. The -BodyParameter can be a hashtable or a PSCustomObject. Let's look at a very simple text-based email.
### Connect to Graph. Variablize authenticated user UPN for use in sending email
Connect-MgGraph -TenantId "xxxxx" -ClientID "yyyyy" -NoWelcome
$myUpn = (Get-MgContext).Account
### Declare params for -BodyParameter
$params = @{
message = @{
subject = "Reminder!"
body = @{
contentType = "text"
content = "Please remember to submit your timesheets!"
}
toRecipients = @(
@{
emailAddress = @{address = "[email protected]"}
}
)
}
}
### Send the email from the authenticated user's mailbox
Send-MgUserMail -UserId $myUpn -BodyParameter $params
NOTE: You can enter everything on one line or within the cmdlet, but variables (especially of this size) are always better!
This will send an email to the IT-Services distribution list telling them to get their timesheets submitted. It is purely text with no formatting. Not the prettiest solution, but it is easy to get going and may be all that is needed in some scenarios. If we need something that needs to look a little more professional, and you don't mind brushing up on your web development skills (like I have to every time I feel like updating this site), then we can use HTML to make something more complex. We can do this by adding an additional multi-line variable and changing the contentType from "text" to "html".
### Declare emailContent for use in params hashtable
$emailContent = @"
<html>
<body>
<style>
p {
font-size: 12px;
}
</style>
<h1><u>REMINDER - TIMESHEETS</u></h1>
<p>
Quick reminder that timesheets <strong>MUST</strong> be submitted...
</p>
</body>
</html>
"@
### Declare params for -BodyParameter
$params = @{
message = @{
subject = "Reminder!"
body = @{
contentType = "html"
content = $emailContent
}
toRecipients = @(
@{
emailAddress = @{address = "[email protected]"}
}
)
}
}
Send-MgUserMail -UserId "[email protected]" -BodyParameter $params
TIP: Since the multi-line variable containing our HTML is encapsulated in double quotes, making it implicit, we can plug in additional variables that will show correctly in our email. This is useful when you need to include values being generated by your script, like generating a user account and sending the login information to that user.
This will send out a similar email to the first example, but with some simple formatting. The style tag can get as complex as you'd like it to be. Be warned that Outlook does not support all modern HTML and CSS elements. Getting an HTML email formatted perfectly can sometimes take trial and error; test your emails by sending them to yourself before letting them loose on others.
Maybe this email is going out to users after a new Payroll system is in place and some may be unfamiliar with the process of submitting their timesheets. You have a fancy PDF file that contains the instructions and would like that to be attached. You're in luck! Send-MgUserMail supports attachments as well. All we need to do is add an additional array literal to our hashtable called "attachments".
The "/sendMail" endpoint that Send-MgUserMail uses does not accept a file as input. It needs to be encoded in Base64, so this won't be as easy as slapping the file path into our hashtable. We'll need to do that conversion ourselves, which can be easily done with [System.Convert]::ToBase64String. Within that, we'll nest [System.IO.File]::ReadAllBytes to first break down the file into bytes to be encoded. Making that a variable will give us something acceptable to plug into our hashtable. The attachments array literal will need a type, a file name (which does not have to match the actual file name), and our encoded file.
### Convert instructions file to Base64, store in variable
$encodedAttachment = $contentBytesInstructionsFile = [System.Convert]::ToBase64String(
[System.IO.File]::ReadAllBytes(C:/path/to/file.pdf)
)
### Declare params for -BodyParameter. Include attachment
$params = @{
message = @{
subject = "Reminder!"
body = @{
contentType = "text"
content = "Please remember to submit your timesheets!"
}
toRecipients = @(
@{
emailAddress = @{address = "[email protected]"}
}
)
attachments @(
@{
"@odata.type" = "#microsoft.graph.fileAttachment"
Name = "Submit Time Instructions.pdf"
ContentBytes = $encodedAttachment
}
)
}
}
Send-MgUserMail -UserId "[email protected]" -BodyParameter $params
There are a few more useful hash values you can use in your hashtable to change settings on your email. We'll go through those all at once and look at an example.
Importance: allows you to select High Importance ("high") or Low Importance ("low"), as you would in the Outlook message options.
SaveToSentItems: allows you to send an email without it being stored in the sending mailbox's sent items. False is the only useful value, as omitting SaveToSentItems will save the email to your sent items normally. This one is uniquely nested outside of the message configuration.
InternetMessageHeaders: lets you set custom email headers.
ccRecipients: Add CC recipient email addresses.
bccRecipients: Add BCC recipient email addresses.
From: Set the mailbox you're sending from with From. Omitting this will default the sending mailbox to your own, while including this will allow you to send from any mailbox you have access to send from, like a shared mailbox.
### Declare params for -BodyParameter
$params = @{
message = @{
subject = "Reminder!"
Importance = "high"
body = @{
contentType = "text"
content = "Please remember to submit your timesheets!"
}
From = @(
@{
emailAddress = @{address = "[email protected]"}
}
)
toRecipients = @(
@{
emailAddress = @{address = "[email protected]"}
}
@{
emailAddress = @{address = "[email protected]"}
}
@{
emailAddress = @{address = "[email protected]"}
}
)
ccRecipients = @(
@{
emailAddress = @{address = "[email protected]"}
}
)
bccRecipients = @(
@{
emailAddress = @{address = "[email protected]"}
}
)
internetMessageHeaders = @(
@{
name = "x-custom-header-group-name"
value = "InformationTechnology"
}
@{
name = "x-custom-header-group-id"
value = "IT01"
}
)
}
saveToSentItems = "false"
}
Send-MgUserMail -UserId "[email protected]" -BodyParameter $params
That is a pretty full-featured email, though the options we've looked at are not exhaustive. As always, check the notes section of the documentation for all possible options. One final caveat I want to note before closing; those of you familiar with the obsolete Send-MailMessage will notice there is not a way to enforce encryption when using Send-MgUserMail like you could with Send-MailMessage and the -UseSsl parameter. This is (as I understand it) by design. The expectation is to use mail flow rules that enforce TLS encryption based on attributes of the email. These can be set up and managed in the Exchange admin center.
Conclusion
Sending emails as part of your scripted processes is extremely useful, whether it's sending information created within your automation, providing periodic reports based on a query, sending customized notifications when events or changes occur, etc. Send-MgUserMail lets you send simple text emails with no frills, or beautifully formatted HTML-based emails. As always, thanks for reading and I'll see you in the next one!