Client SMTP Email Submission & Graph API – Azure

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.

New-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.

#Get Authorization Token
$TenantId = ""
$AppId = ""
$AppSecret = ""

$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
$token = ($tokenRequest.Content | ConvertFrom-Json).access_token
$Headers = @{
    'Content-Type'  = "application\json"
    'Authorization' = "Bearer $Token"
}


# Create & Send Message
$From = "Azure-Pipeline-Notifications@domain.com"
$Recipient = "recipient@domain.com"
$Subject = "<Email Subject>"
$EmailBody = "<Email Body>"
$MessageSplat = @{
    "URI"         = "https://graph.microsoft.com/v1.0/users/$From/sendMail"
    "Headers"     = $Headers
    "Method"      = "POST"
    "ContentType" = 'application/json'
    "Body"        = (@{
            "message" = @{
                "subject"      = $Subject
                "body"         = @{
                    "contentType" = 'HTML'
                    "content"     = $EmailBody
                }
                "toRecipients" = @(
                    @{
                        "emailAddress" = @{"address" = $Recipient }
                    } )
            }
        }) | ConvertTo-JSON -Depth 6
}
Invoke-RestMethod $MessageSplat

Here is the result!

Example HTML Output

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.

Function Send-Email {
    <#
    .SYNOPSIS
        Send emails via O365.
    .DESCRIPTION
        Send emails via O365 using the Send.Mail Graph API.  Parameter values are expected to be variables.
    .PARAMETER TenantId
        Tenant ID found in Azure.
    .PARAMETER AppId
        ID of the App Registration.
    .PARAMETER AppSecret
        App Registration client key.
    .PARAMETER From
        Email sender address.
    .PARAMETER Recipient
        Recipient address, user, group or shared mailbox etc.
    .PARAMETER Subject
        Email Subject value.
    .PARAMETER Body
        Email body value.
    .LINK
        https://github.com/smctighevcp
    .EXAMPLE
        PS C:\> Send-Email -TenantId $value -AppId $value -AppSecret $value -From $value -Recipient $value -Subject $value -Body $value
        Takes the variable input and send a email to the specified recipients.
    .NOTES
        Author: Stephan McTighe
        Website: stephanmctighe.com
        Created: 10/03/2022

        Change history:
        Date            Author      V       Notes
        10/03/2022      SM          1.0     First release
    #>
    #Requires -Version 5.1

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string] $TenantId,
        [Parameter(Mandatory)]
        [string] $AppId,
        [Parameter(Mandatory)]
        [string] $AppSecret,
        [Parameter(Mandatory)]
        [string] $From,
        [Parameter(Mandatory)]
        [string] $Recipient,
        [Parameter(Mandatory)]
        [string] $Subject,
        [Parameter(Mandatory)]
        [string] $EmailBody
    )
    Begin {

        #
        $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
        #
        $token = ($tokenRequest.Content | ConvertFrom-Json).access_token
        $Headers = @{
            'Content-Type'  = "application\json"
            'Authorization' = "Bearer $Token"
        }
    }
    Process {
        # Create & Send Message
        $MessageSplat = @{
            "URI"         = "https://graph.microsoft.com/v1.0/users/$From/sendMail"
            "Headers"     = $Headers
            "Method"      = "POST"
            "ContentType" = 'application/json'
            "Body"        = (@{
                    "message" = @{
                        "subject"      = $Subject
                        "body"         = @{
                            "contentType" = 'HTML'
                            "content"     = $EmailBody
                        }
                        "toRecipients" = @(
                            @{
                                "emailAddress" = @{"address" = $Recipient }
                            } )
                    }
                }) | ConvertTo-JSON -Depth 6
        }
        Invoke-RestMethod @MessageSplat
    }
    end {

    }
}

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!

https://platform.twitter.com/widgets.js

Thanks for reading!