Microsoft Offers Raw APIs and Two SDK Methods
SMTP AUTH remains an Exchange Online connection protocol that supports basic authentication. Microsoft was forced to exclude it from the set of protocols it is in the final stages of retiring. Too many organizations have too many devices and apps that send email via Exchange Online for Microsoft to pull the plug on SMTP AUTH. However, there’s no doubt that this will happen. The only question is when.
The replacement is to use the Microsoft Graph APIs to create and send email. Over the last year or so, I’ve been investigating the available methods. Microsoft offers the following Graph-based options:
- The Graph sendMail API.
- The Send-MgUserMessage cmdlet in the Microsoft Graph PowerShell SDK.
- The Send-MgUserMail cmdlet in the Microsoft Graph PowerShell SDK.
Why does the Microsoft Graph PowerShell SDK include two methods to do the same job? One answer lies in the way that Microsoft generates SDK cmdlets automatically using the AutoRest process. If an API exists, the SDK gets a cmdlet. And because the Graph includes the message: send API and the user: sendMail API, the SDK glories in two cmdlets that both send email via Exchange Online. But more importantly, each cmdlet serves a different purpose. We’ll get to that soon.
The Independent Send-EmailMessage cmdlet
If you don’t fancy Microsoft’s cmdlets, Przemyslaw Klys, the developer of the well-respected PSWriteHTML PowerShell module, offers his own take on the topic in the Send-EmailMessage cmdlet. This cmdlet is in the splendidly-named Mailozaurr module.
Unlike the Microsoft Graph PowerShell SDK cmdlets, Przemyslaw’s work exhibits the hallmarks of some thoughtful programming to make it as easy as possible to create and send messages. For instance, Microsoft’s cmdlets require special processing for large attachments (anything over 3 MB), which can be a real hindrance (see this article by MVP Glen Scales). The Send-EmailMessage cmdlet handles large attachments automatically, which is exactly the way Microsoft’s cmdlets should do.
The reason why people use Microsoft’s APIs is because they are Microsoft’s APIs. Although a better independent alternative might exist, organizations might be loath to write code based on a module that isn’t formally supported. Of course, in-house developers could do exactly what Przemyslaw did and build a better wrapper around the Graph APIs, but I can’t see many rushing to do that. In any case, take a look at the Mailozaurr module and make your own mind up.
Deciding Between the Two SDK Cmdlets
Given the choice between the two SDK cmdlets, which one should you use? Previously, I covered how to send emails using the Send-MgUserMessage cmdlet. The Send-MgUserMail cmdlet is easier to use because you can create and send a message with a single command instead of creating a draft message first (with New-MgUserMessage) and then sending it.
By itself, creating and sending in one motion probably makes Send-MgUserMail a good choice for most scenarios when apps need to send email. Typically, these messages are something like a notification to let someone know about some information, like a job finishing or something to check (like highly-permissioned Azure AD apps).
Send-MgUserMessage does have some tricks up its sleeve. Because it sends an already-created message, it can handle scenarios like replies and message forwards. However, I think relatively few apps need to process draft messages or respond to inbound mail, this capability might not be as useful as it seems.
Testing Send-MgUserMail
To test how easy it is to use the Send-MgUserMail cmdlet, I converted the sample script I used with Send-MgUserMessage. This script sends a welcome message to new employees. It uses the Get-ExoMailbox cmdlet to find mailboxes created within the last week. This is logical because if we’re going to send a welcome message, it needs to go to a mailbox. If you didn’t want to load the Exchange Online management module, something similar could be done with:
$CheckDate = (Get-Date).AddDays(-7) [array]$Users = Get-MgUser -All -Filter "userType eq 'Member'" $Users = $Users | Where-Object {$_.CreatedDateTime -gt $CheckDate}
This command works perfectly well, but its performance is not going to be sparkling in larger tenants. The Get-MgUser cmdlet doesn’t support filtering against an account’s creation date, so to find the set of recently created accounts, we must fetch all accounts and then apply a client-side filter. By comparison, the Get-ExoMailbox cmdlet can happily use a server-side filter, meaning that Exchange Online only returns the set of target mailboxes.
[array]$Users = (Get-ExoMailbox -Filter "WhenMailboxCreated -gt '$CheckDate'" -RecipientTypeDetails UserMailbox -ResultSize Unlimited -Properties WhenMailboxCreated | Select WhenMailboxCreated, DisplayName, UserPrincipalName, PrimarySmtpAddress)
When I wrote about Send-MgUserMessage, I noted that the cmdlet uses arrays to pass recipients for the message and said that the SDK developers could do us all a favor by making this part of building out message properties easier. A PowerShell function does the trick and can handle To, Cc, and Bcc recipients.
Function Populate-MessageRecipient { # Build a list of recipients for a message [cmdletbinding()] Param( [array]$ListOfAddresses ) ForEach ($SMTPAddress in $ListOfAddresses) { @{ emailAddress = @{address = $SMTPAddress} } } }
To add recipients, we create an array holding the SMTP address for each recipient and put it through the function to create the array to add to the message properties. For example, this code adds two recipients (a distribution group and a mailbox) as Cc recipients.
$CcRecipientList = @( 'Tenant.Admins@office365itpros.com' 'Kim.Akers@Office365itpros.com' ) [array]$MsgCcRecipients = Populate-MessageRecipient -ListOfAddresses $CcRecipientList
The remainder of the script:
- Prepares an attachment encoded in Base64.
- Customizes the message content for the recipient and builds an HTML message body.
- Populates the set of message properties (subject, To recipients, Cc recipients, attachment, and the message body).
- Populates two message parameters (save the message to sent items and ask for a delivery receipt).
- Sends the message with Send-MgUserMail.
Here’s the code (the full script is available from GitHub):
$MsgAttachment = @( @{ "@odata.type" = "#microsoft.graph.fileAttachment" Name = ($AttachmentFile -split '\\')[-1] ContentType = "text/plain" ContentBytes = $EncodedAttachmentFile } ) ForEach ($User in $Users) { $ToRecipientList = @( $User.PrimarySmtpAddress ) [array]$MsgToRecipients = Populate-MessageRecipient -ListOfAddresses $ToRecipientList Write-Host "Sending welcome email to" $User.DisplayName # Customize the message $htmlHeaderUser = "<h2>New User " + $User.DisplayName + "</h2>" $HtmlMsg = "</body></html>" + $HtmlHead + $htmlheaderuser + $htmlbody + "<p>" # Construct the message body $MsgBody = @{ Content = "$($HtmlMsg)" ContentType = 'html' } $Message = @{subject = $MsgSubject} $Message += @{toRecipients = $MsgToRecipients} $Message += @{ccRecipients = $MsgCcRecipients} $Message += @{attachments = $MsgAttachment} $Message += @{body = $MsgBody} $Params = @{'message' = $Message} $Params += @{'saveToSentItems' = $True} $Params += @{'isDeliveryReceiptRequested' = $True} Send-MgUserMail -UserId $MsgFrom -BodyParameter $Params } Write-Host "All done. Messages sent!"
Everything works and our new employees receive a nice welcome message (Figure 1).
Dealing with Permissions
Using the Send-MgUserMessage and Send-MgUserMail cmdlets both require the Mail.Send Graph permission. Creating the draft message for Send-MgUserMessage to dispatch requires the Mail.ReadWrite permission. These permissions apply to every mailbox in the tenant. In other words, a background app equipped with these permissions that uses certificate-based authentication can write into every mailbox and send email from every mailbox. With that kind of access, care must be taken to restrict the ability of scripts running with apps or using Azure Automation runbooks (with or without a managed identity) to access mailboxes.
The right way to do that is with an application access policy. These policies dictate the mailboxes an app can access, meaning that you can limit apps to some app-specific mailboxes and keep them well away from user mailboxes.
Decision Time
It’s nice to have a choice. Developers who update scripts to remove the dependency on SMTP AUTH have at least four Graph-based options. For me, I think I shall use Send-MgUserMail whenever possible, if only because it’s the easiest way to send a message. At least, the easiest Microsoft-based method. The independent Send-EmailMessage cmdlet looks like an attractive option too!
Hello
Great article and very informative.
I have created a script for new starters that does a number of actions then uses the built-in Send-MailMessage cmdlet to send a few emails, a welcome email to the new starter and couple other confirmation emails to other recipients.
The issue we now have is that they want the welcome emal to be sent on the new starters first day rather then when we process the script so i am looking at how we can achieve that, basically deferring/delaying the delivery of that welcome email. I can’t use Start-Sleep as it is not practical (for one we do multiple new starters in one batch and the start dates vary) and would rather employ a setting like Outlook desktop app has for deferring emails.
I started looking at the Graph API cmdlets as the built-in Send-MailMessage does not support it but it seems neither do these. Do you know if there is a way to achieve this using the new cmdlets or another practical way which is just in the script and NOT relying on doing something else, like scheduling a task to send?
Regards
Z
The Real Person!
Author Tony Redmond acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
Use an Azure Automation runbook to run the script to generate email to the new employees and schedule it to run at the end of the working day. https://practical365.com/use-azure-automation-exchange-online/
How do we go about attaching multiple attachments? I have tried to add an additional key but it detects this as a issue and does not allow it.
The Real Person!
Author Tony Redmond acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
In the past, I have used a function to create the table of attachments for a message. Here’s what I use… the input is a simple array of attachment names.
Function Populate-Attachments {
[cmdletbinding()]
Param(
[array]$ListOfAttachments )
ForEach ($Attachment in $Attachments) {
Write-Host “Processing” $Attachment
$EncodedAttachmentFile = [Convert]::ToBase64String([IO.File]::ReadAllBytes($Attachment))
@{
“@odata.type”= “#microsoft.graph.fileAttachment”
name = ($Attachment -split ‘\\’)[-1]
contentBytes = $EncodedAttachmentFile
contentType = “text/plain”
}
}
}