powershell sendmail function

With modern companies using hosted email solutions, Microsoft has deprecated the use of the Send-MailMessage PowerShell cmdlet. The supported to way to send emails from a PowerShell script is with the Microsoft Graph API. Unfortunately, this is not as simple as using an on-premises SMTP server, but it's much more secure. And by building a proper PowerShell function, it's just as easy to integrate within your scripts. This does require an Office365 tenant and subscription. I will outline the steps and give an example of how to send emails with this method.


Create a Registered Sendmail App in Microsoft Entra ID

  1. Login to the Azure portal at https://portal.azure.com and navigate to the Microsoft Entra ID section.

    Make sure to note down your Tenant ID in the overview page

    Entra ID Tenant ID

  2. On the left-hand side, navigate to Enterprise Applications and click Create your Own Application.

    Enterprise Applications

  3. Give your Application a Name, select non-gallery as the type, and click Create.



  4. Note the Application ID for your application

    Application ID

  5. Click the Permissions tab under Security and then click the App Registrations link to get to the API permissions page
  6. Click Add a permission and select Microsoft Graph

    Add permission

  7. Choose Application Permissions and type in Mail.Send and add the permission

    This gives the application the ability to send as ANY mailbox in your tenant. Don't worry, we will be limiting this later to a specific group.


    Mail.send permission

  8. Click Certificates and Secrets and add a new client secret. Make sure to copy the Value. The best way to store it is within a PowerShell SecretStore. Click the link for more information on how to do that.

Limit Application Permission 

Now we need to limit the Mail.send permission that your application has. The best way to do this is with an Application Access Policy. An application access policy does the following...

  • Defines deny or allow access
  • Covers one or many Entra ID applications
  • Applies to Exchange Web Services and Microsoft Graph API calls made to mailboxes for members of a mail-enabled security group or individual users

It makes the most sense to apply the policy to a mail-enabled security group and control access by adding or removing to and from that group. 

  1. Create your mail-enabled security group. This process differs based on your environment.
  2. Add the desired mailboxes to the new security group you created. The sendmail application will be able to send as any mailbox in this group.
  3. Connect to Exchange Online PowerShell. The Exchange Online PowerShell module is required for this. 
    Connect-ExchangeOnline -UserPrincipalName This email address is being protected from spambots. You need JavaScript enabled to view it.
  4. Now add your application access policy. 
    New-ApplicationAccessPolicy -AppId XXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX -PolicyScopeGroupId This email address is being protected from spambots. You need JavaScript enabled to view it. -AccessRight
    RestrictAccess -Description "Restrict access to app allowed to send email using the Graph SendMail API"

    This policy applies only to the AppId (Retrieved from the overview tab under enterprise applications). It allows access only to mailboxes that are members of the SendMailPowershell security group. If you attempt to send as any other mailbox, the SendMail API will return a 403 error. 

  5. Test the Application Access Policy with the following command...
    Test-ApplicationAccessPolicy -Identity This email address is being protected from spambots. You need JavaScript enabled to view it. -AppId XXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX

Changes to Application Access Policies can take up to 1 hour to take effect. If the test indicates the app has access to the mailbox, but you are receiving a 403 error, wait a bit longer and try again.


Create your PowerShell Function

Now it's time to create your PowerShell function. The first step is to create an authorization and retrieve an access token. 

Retrieve Access Token with PowerShell

Define your function and parameters first. Then add your $TenantID, $AppId, and $AppSecret. Finally construct the URI and body and make the API call for the token.
See the following code snippet...

function sendmail{
param ( 
 [string]$MsgFrom, 
 [array]$Recipients,
 [string]$MsgSubject, 
 [string]$emailbody)

$AppId = "XXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX"
$AppSecret = Get-Secret -name sendmail-API-key -AsPlainText
$TenantId = "XXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX"
# Construct URI and body needed for authentication
$uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
$body = @{
    client_id     = $AppId
    scope         = "https://graph.microsoft.com/.default"
    client_secret = $AppSecret
    grant_type    = "client_credentials"
}
$tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing
# Unpack Access Token
$token = ($tokenRequest.Content | ConvertFrom-Json).access_token
$Headers = @{
            'Content-Type'  = "application/json"
            'Authorization' = "Bearer $Token" }

The $AppID$TenantID, and $AppSecret will be different in your organization. My code is using the PowerShell SecretStore to retrieve the App Secret. This code constructs the URI and body, makes a token request to the Microsoft Graph API and unpacks it. Then headers are constructed with the Bearer Token for use later. 

Define Email Parameters and Call the SendMail API

Now that you have a valid authorization token, you are able to call the API to send an email. Each mailbox uses a different URI. Take a look at the rest of the functions code...

$torecipients = @()
foreach ($r in $Recipients){
    $torecipients += @{ 
    'emailaddress' =  @{ address = $r}}
    }

# Create message body and properties and send
      $messagebody = @{
          message = @{
            subject = $MsgSubject
            body    = @{
                content = $emailbody
                contenttype = 'html'
                    }
            toRecipients = $torecipients
            }
           
  savetosentitems = 'false'   
} | ConvertTo-JSON -Depth 6

   # Send the message
Invoke-RestMethod -Uri https://graph.microsoft.com/v1.0/users/$MsgFrom/sendMail -Method POST -Headers $HeadersEmail -Body $messagebody 
#End Function
}

This part loops through the recipients, constructs the message body, and then makes a call to the specific URI for the mailbox in the $MsgFrom parameter.

Congrats! You have created a PowerShell function for sending email!

Copy and paste this function into any PowerShell script at the top and you are able to send email in the following way...

$MailParams = @{
            msgfrom = This email address is being protected from spambots. You need JavaScript enabled to view it.'
            recipients = (This email address is being protected from spambots. You need JavaScript enabled to view it.', This email address is being protected from spambots. You need JavaScript enabled to view it.') 
            MsgSubject = "My Message Subject"
            emailBody = $tableformat + '</body></html>'
            }
sendmail @MailParams

Want to add attachments to your email? See the next section...

Add Attachments to your SendMail Function

Unfortunately, with the Microsoft Graph SendMail API, attachments are limited to 3MB. There are other methods if you need to attach larger files, but I won't be covering them with this guide. Attachments must be encoded with Base64.

Add the attachment parameter to the sendmail function. It will be a [string] and should be the file path of where the attachment is located. You will use the variable $attachmentfile in the code.

The following is the full SendMail function including the attachment parameter.

function sendmail{
param ( 
 [string]$MsgFrom, 
 [array]$Recipients,
 [string]$MsgSubject, 
 [string]$emailbody,
 [string]$attachmentfile)

$AppId = "XXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX"
$AppSecret = $sendmailapikey
$TenantId = "XXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX"
# Construct URI and body needed for authentication
$uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
$body = @{
    client_id     = $AppId
    scope         = "https://graph.microsoft.com/.default"
    client_secret = $AppSecret
    grant_type    = "client_credentials"
}
$tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing
# Unpack Access Token
$token = ($tokenRequest.Content | ConvertFrom-Json).access_token
$HeadersEmail = @{
            'Content-Type'  = "application/json"
            'Authorization' = "Bearer $Token" }
#$ccRecipient1 = $null
#$ccRecipient2 = $null

#Define attachment 

if($attachmentfile) {
    $ContentBase64 = [convert]::ToBase64String( [system.io.file]::readallbytes($AttachmentFile))
}

$torecipients = @()
foreach ($r in $Recipients){
    $torecipients += @{ 
    'emailaddress' =  @{ address = $r}}
    }

if($attachmentfile) {
# Create message body and properties and send
      $messagebody = @{
          message = @{
            subject = $MsgSubject
            body    = @{
                content = $emailbody
                contenttype = 'html'
                    }
            toRecipients = $torecipients
         attachments = @(
             @{
              "@odata.type" = "#microsoft.graph.fileAttachment"
              "name" = $attachmentfile
              "contentBytes" = $ContentBase64 })  
           }      
      savetosentitems = 'false'   
    }  | ConvertTo-JSON -Depth 6
}
else {
$messagebody = @{
          message = @{
            subject = $MsgSubject
            body    = @{
                content = $emailbody
                contenttype = 'html'
                    }
            toRecipients = $torecipients
           }      
      savetosentitems = 'false'   
    }  | ConvertTo-JSON -Depth 6
}
# Send the message
Invoke-RestMethod -Uri https://graph.microsoft.com/v1.0/users/$MsgFrom/sendMail -Method POST -Headers $HeadersEmail -Body $messagebody 
#End Function
}

After adjusting tenant and app specific variables, you can copy and paste this function into any script and use the following to send an email with an attachment!

$MailParams = @{
            msgfrom = This email address is being protected from spambots. You need JavaScript enabled to view it.'
            recipients = (This email address is being protected from spambots. You need JavaScript enabled to view it.', This email address is being protected from spambots. You need JavaScript enabled to view it.') 
            MsgSubject = "Subject and Attached!"
            emailBody = "Take a look at the attached file."
            attachmentfile = "\\fileserver\shared\report.csv"
            }
sendmail @MailParams

Conclusion

We went over creating a custom SendMail application in the Microsoft Entra ID portal. We assigned application permissions and generated a client secret. Then we used an application access policy to limit the scope to one mail-enabled security group. Using this application, we created a function in PowerShell to call the Microsoft Graph API and send an email message. This function can be copy and pasted into any PowerShell script and should come in very handy. Don't forget the App Secret has an expiration and you will need to generate another when it expires. Hopefully this guide was useful and happy scripting!

No comments