Client SMTP Email Submission & Graph API - Azure

Posted by Stephan McTighe on 15 Mar 2022

I recently began playing with Azure DevOps Pipelines as a way to automate various aspects of my lab. As part of this I wanted to send email notifications that I could customize the content of, which I couldn’t get from the built-in notification capability.

Since the PowerShell cmdlet; Send-Mail Message is obsolete, I began investigating alternatives which is when I came across this article and decided to give it ago and share!

Overview

This solution is based on the usage of Microsoft’s Graph API (Send.Mail) and App Registrations being leveraged with PowerShell. Details of the API can be found here.

An Application or Service can call the email sending functionality by passing required data as parameters into the PowerShell script (or Function!) to provide a reusable approach to sending email.

You will need a couple of things for this to work, lets get started.

Configuration

Create a Shared mailbox

You will need a ‘From’ mailbox to use as the sending address. I will be using a Shared mailbox in O365 via my Business Basic subscription (Around £4.50 per user per month). Shared mailbox’s don’t require a license (Under 50GB), hence not costing you anything additional!

Head over to your Exchange Admin Center and select Recipients, Mailboxes, and select Add a shared mailbox.

Provide a Display Name and Email Address, as well as selecting a domain.

That’s the mailbox ready to go!

Create App Registration

Next we need to set up an App Registration in your tenancy.

In the Azure Portal, search for and select Azure Active Directory followed by App registrations.

Click New Registration…

…and provide your application a name. I have also stuck with the default single tenant option.

Once created, you will be able to see information that you will need later. Specifically the Application ID and the Tenant ID. You will need a third piece of information, a Secret Key. You can generate one by clicking client credentials.

Click Client secrets and select New client secret

Provide a meaningful name and select the duration you want the secret to be valid for.

You will then see your secret key.

You will need to take a copy of this key now and store it securely as you wont be able to get the key again without creating a new one.

We now need to provide some permissions. In this case we are wanting to be able to send an email.

Firstly, click API permissions and then Add a permission.

Select Graph API.

Select Application permissions and scroll down until you see Mail and select the Mail.Send option, and finally click Add permission at the bottom.

You will then notice that it require Admin consent. Click the Grant consent for ‘org’ option and confirm the prompt.

Things to consider!

An application that uses this will have access to send an email from any mailbox. You need to carefully consider the risks and mitigations.

You can limit which recipients can be sent to, by applying an Application Access Policy. More information here. Note for shared mailboxes, you need to add them to a mail enabled security group and reference that with the PolicyScopeGroupID parameter.

1New-ApplicationAccessPolicy -AppId -PolicyScopeGroupId

The Code

There are 2 main sections, the first being the acquisition of an authorization token. Using the 3 values called out in the App Registration section earlier, we need to populate the TenantID, AppID and App Secret variables. Ideally you would want to be retrieving this from a secure location such as an Azure Key Vault (Look out of a future post on this!).

The second section is the collation of values required to send the email. Again you need to populate the variable values for the From, Recipient, Subject and Message Body which are then passed into a Invoke-RestMethod cmdlet with the URI of the API.

If you are using this as part of an automated solution, you aren’t going to be manually entering values, you are likely to be passing the values in from the rest of your code or pipeline.

 1#Get Authorization Token
 2$TenantId = ""
 3$AppId = ""
 4$AppSecret = ""
 5
 6$uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
 7$body = @{
 8    client_id     = $AppId
 9    scope         = "https://graph.microsoft.com/.default"
10    client_secret = $AppSecret
11    grant_type    = "client_credentials"
12}
13$tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing
14$token = ($tokenRequest.Content | ConvertFrom-Json).access_token
15$Headers = @{
16    'Content-Type'  = "applicationjson"
17    'Authorization' = "Bearer $Token"
18}
19
20# Create & Send Message
21$From = "[email protected]"
22$Recipient = "[email protected]"
23$Subject = "<Email Subject>"
24$EmailBody = "<Email Body>"
25$MessageSplat = @{
26    "URI"         = "https://graph.microsoft.com/v1.0/users/$From/sendMail"
27    "Headers"     = $Headers
28    "Method"      = "POST"
29    "ContentType" = 'application/json'
30    "Body"        = (@{
31            "message" = @{
32                "subject"      = $Subject
33                "body"         = @{
34                    "contentType" = 'HTML'
35                    "content"     = $EmailBody
36                }
37                "toRecipients" = @(
38                    @{
39                        "emailAddress" = @{"address" = $Recipient }
40                    } )
41            }
42        }) | ConvertTo-JSON -Depth 6
43}
44Invoke-RestMethod $MessageSplat

Here is the result!

I have also put together a PowerShell Function that can be used as part of a larger piece of code. This way you are able to utilize it in a more efficient and reusable way.

 1Function Send-Email {
 2    <#
 3    .SYNOPSIS
 4        Send emails via O365.
 5    .DESCRIPTION
 6        Send emails via O365 using the Send.Mail Graph API.  Parameter values are expected to be variables.
 7    .PARAMETER TenantId
 8        Tenant ID found in Azure.
 9    .PARAMETER AppId
10        ID of the App Registration.
11    .PARAMETER AppSecret
12        App Registration client key.
13    .PARAMETER From
14        Email sender address.
15    .PARAMETER Recipient
16        Recipient address, user, group or shared mailbox etc.
17    .PARAMETER Subject
18        Email Subject value.
19    .PARAMETER Body
20        Email body value.
21    .LINK
22        https://github.com/smctighevcp
23    .EXAMPLE
24        PS C:\> Send-Email -TenantId $value -AppId $value -AppSecret $value -From $value -Recipient $value -Subject $value -Body $value
25        Takes the variable input and send a email to the specified recipients.
26    .NOTES
27        Author: Stephan McTighe
28        Website: stephanmctighe.com
29        Created: 10/03/2022
30
31        Change history:
32        Date            Author      V       Notes
33        10/03/2022      SM          1.0     First release
34    #>
35    #Requires -Version 5.1
36
37    [CmdletBinding()]
38    param (
39        [Parameter(Mandatory)]
40        [string] $TenantId,
41        [Parameter(Mandatory)]
42        [string] $AppId,
43        [Parameter(Mandatory)]
44        [string] $AppSecret,
45        [Parameter(Mandatory)]
46        [string] $From,
47        [Parameter(Mandatory)]
48        [string] $Recipient,
49        [Parameter(Mandatory)]
50        [string] $Subject,
51        [Parameter(Mandatory)]
52        [string] $EmailBody
53    )
54    Begin {
55
56        #
57        $uri = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
58        $body = @{
59            client_id     = $AppId
60            scope         = "https://graph.microsoft.com/.default"
61            client_secret = $AppSecret
62            grant_type    = "client_credentials"
63        }
64
65        $tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing
66        #
67        $token = ($tokenRequest.Content | ConvertFrom-Json).access_token
68        $Headers = @{
69            'Content-Type'  = "applicationjson"
70            'Authorization' = "Bearer $Token"
71        }
72    }
73    Process {
74        # Create & Send Message
75        $MessageSplat = @{
76            "URI"         = "https://graph.microsoft.com/v1.0/users/$From/sendMail"
77            "Headers"     = $Headers
78            "Method"      = "POST"
79            "ContentType" = 'application/json'
80            "Body"        = (@{
81                    "message" = @{
82                        "subject"      = $Subject
83                        "body"         = @{
84                            "contentType" = 'HTML'
85                            "content"     = $EmailBody
86                        }
87                        "toRecipients" = @(
88                            @{
89                                "emailAddress" = @{"address" = $Recipient }
90                            } )
91                    }
92                }) | ConvertTo-JSON -Depth 6
93        }
94        Invoke-RestMethod @MessageSplat
95    }
96    end {
97
98    }
99}

Keep an eye out for a future blog post on how I am using this as part of an Azure DevOps Pipeline! This will include passing in variables within the pipeline as well as retrieving secrets from an Azure Key Vault.

If you like my content, consider following me on Twitter so you don’t miss out!

Follow @vStephanMcTighe

Thanks for reading!