Send Mail with Azure DevOps REST API to a user group

Niklas OhlroggeNiklas Ohlrogge
6 min read

Are you looking for a way to send emails programmatically from within an Azure DevOps pipeline without setting up any SMTP server? The ADO REST API offers a simple and powerful solution for sending emails using the sendmail RESTful method which can be easily integrated into a PowerShell script. Unfortunately, Microsoft's Documentation doesn't provide an example.

In this blog entry, we will walk through the process of getting the required user information based on a group name, preparing the REST API parameters and body and finally using the command to send emails from your Azure DevOps pipeline, including the challenges I encountered and the solution I ultimately found.

Built-in solutions for sending emails don't always work

During the development of our pipelines, we were running into a few limitations of Azure DevOps. One of the main issues we faced during our pipeline development was the inability of Azure DevOps to send emails to nested Azure AD groups. Additionally, manual validation does not allow for the passing of an array of users or a nested Azure AD group as a variable for notification purposes. To address these challenges, we implemented the following solution...

Description of the script

Variables

The PowerShell script is executed from within an Azure DevOps pipeline. We will use the following predefined environment variables:

  • $env:SYSTEM_ACCESSTOKEN

  • $env:SYSTEM_TEAMFOUNDATIONSERVERURI

  • $env:SYSTEM_TEAMPROJECTID

  • $env:SYSTEM_TEAMPROJECT

Additionally, the following parameters are required, they must be part of the above-defined ADO team project:

  • $groupName

  • $workItemId

The following variables are defined based on the variables above:

  • $adoHeader = @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$env:SYSTEM_ACCESSTOKEN")) }

  • $organization = ($env:SYSTEM_TEAMFOUNDATIONSERVERURI-split "/")[3]

Get recipients' data based on ADO group name

To send emails we require the recipient's $emailAddresses and team foundations Ids ($tfIds). To receive this data we have to execute a few calls from the REST API graph.

The script starts with a GET request to the ADO API to retrieve the project descriptor for the project specified in the SYSTEM_TEAMPROJECTID environment variable. This descriptor is then used to make another GET request to retrieve information about groups within the project. From the returned list of groups, the script filters for the group with a display name that matches the value of the $groupName variable:

$uriGetProjectDescriptor = ("https://vssps.dev.azure.com/{0}/_apis/graph/descriptors/{1}" -f $organization, $env:SYSTEM_TEAMPROJECTID)
$projectDescriptor = (Invoke-RestMethod -Uri $uriGetProjectDescriptor -Method GET -Headers $adoHeader).value

$uriGetGroups = ("https://vssps.dev.azure.com/{0}/_apis/graph/groups?scopeDescriptor={1}" -f $organization, $projectDescriptor)
$group = (Invoke-RestMethod -Uri $uriGetGroups -Method GET -Headers $adoHeader).value | Where-Object { $_.displayname -eq $groupName }

Next, the script uses the descriptor of the selected group to make another GET request to retrieve a list of memberships for the group. The member descriptors from this list are extracted and used to make a final GET request to the ADO API to retrieve identity information about the members themselves.

$uriGetMemberships = ("https://vssps.dev.azure.com/{0}/_apis/graph/Memberships/{1}?direction=Down" -f $organization, $group.descriptor)
$memberShipsList = Invoke-RestMethod -Uri $uriGetMemberships -Method GET -Headers $adoHeader

$subjectDescriptors = $memberShipsList.value | Select-Object -Property memberDescriptor | Select-Object -ExpandProperty memberDescriptor
$uriGetMembers = ("https://vssps.dev.azure.com/{0}/_apis/identities?subjectDescriptors={1}&api-version=6.0" -f $organization, $($subjectDescriptors -join ","))
$members = (Invoke-RestMethod -Uri $uriGetMembers -Method GET -Headers $adoHeader).value

Finally, the script iterates over the returned members and extracts their email addresses and team foundation IDs, adding them to the $emailAddresses and $tfIds arrays, respectively.

$emailAddresses = @()
$tfIds = @()
foreach ($member in $members) {
    $emailAddresses += $member.properties.Mail.'$value'
    $tfIds += $member.id
}

Request body

The requestBody represents the body of the email that will be sent. It looks as follows:

$requestBody = @{ 
    message   = @{
        to      = @{
            emailAddresses = $emailAddresses
            tfIds          = $tfIds
        }
        cc      = @{}
        replyTo = @{}
        body    = "This is a test mail"
        subject = "[Azure DevOps] Test mail"
    }
    projectId = $env:SYSTEM_TEAMPROJECTID
    ids       = @($workItemId)
}

The message property is an object that contains information about the email itself, including the body of the email, the subject, and the recipients. The to property is an object that contains information about the primary recipients of the email. It has as properties the arrays that were generated before: emailAddresses and tfIds. The cc and replyTo properties are both empty.

Please note that the body of the message doesn't accept HTML even though described differently in the original documentation, e. g. it converts symbols like < or > into &lt; or &gt; . But it's possible to add linebreaks with `n or just insert plain URLs.

The projectId property needs to be provided as sting and the work item id within an array, so it could also be possible here to mention multiple work item ids.

During development I found out that a few properties are required: The email will only be sent if projectId, ids, cc, and replyTo are provided with values.

Sending the mail

Now we've everything prepared to send the email with the Azure DevOps REST API. So let's prepare the arguments and splat them.

$arguments = @{
    Uri         = ("{0}{1}/_apis/wit/sendmail?api-version=7.0" -f $env:SYSTEM_TEAMFOUNDATIONSERVERURI, $env:SYSTEM_TEAMPROJECT)
    Method      = 'POST' 
    Headers     = $adoHeader 
    Body        = ConvertTo-Json -InputObject $requestBody -Depth 5
    ContentType = 'application/json; charset=utf-8'
}
Invoke-RestMethod @arguments

The arguments variable is an object that contains several properties that define the details of the HTTP request. These properties include Uri, Method, Headers, Body, and ContentType.

The Invoke-RestMethod take the arguments object. The @ symbol before the arguments variable indicates that the contents of the object will be splatted, meaning that the properties of the object will be treated as separate arguments rather than being passed as a single object.

Result

Finally, we would receive the following email. It will contain the body text as a Note and the array of work items as a table including the title.

Result of an email send with the script

Complete code

And here you find the complete script for you to copy and work with:

# Required variables
$env:SYSTEM_ACCESSTOKEN
$env:SYSTEM_TEAMFOUNDATIONSERVERURI 
$env:SYSTEM_TEAMPROJECTID
$env:SYSTEM_TEAMPROJECT 
$groupName
$workItemId

# Derived variables
$adoHeader = @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$env:SYSTEM_ACCESSTOKEN")) }
$organization = ($env:SYSTEM_TEAMFOUNDATIONSERVERURI -split "/")[3]

# Get recipients data based on ADO group name  
$uriGetProjectDescriptor = ("https://vssps.dev.azure.com/{0}/_apis/graph/descriptors/{1}" -f $organization, $env:SYSTEM_TEAMPROJECTID)
$projectDescriptor = (Invoke-RestMethod -Uri $uriGetProjectDescriptor -Method GET -Headers $adoHeader).value

$uriGetGroups = ("https://vssps.dev.azure.com/{0}/_apis/graph/groups?scopeDescriptor={1}" -f $organization, $projectDescriptor)
$group = (Invoke-RestMethod -Uri $uriGetGroups -Method GET -Headers $adoHeader).value | Where-Object { $_.displayname -eq $groupName }

$uriGetMemberships = ("https://vssps.dev.azure.com/{0}/_apis/graph/Memberships/{1}?direction=Down" -f $organization, $group.descriptor)
$memberShipsList = Invoke-RestMethod -Uri $uriGetMemberships -Method GET -Headers $adoHeader

$subjectDescriptors = $memberShipsList.value | Select-Object -Property memberDescriptor | Select-Object -ExpandProperty memberDescriptor
$uriGetMembers = ("https://vssps.dev.azure.com/{0}/_apis/identities?subjectDescriptors={1}&api-version=6.0" -f $organization, $($subjectDescriptors -join ","))
$members = (Invoke-RestMethod -Uri $uriGetMembers -Method GET -Headers $adoHeader).value

$emailAddresses = @()
$tfIds = @()
foreach ($member in $members) {
    $emailAddresses += $member.properties.Mail.'$value'
    $tfIds += $member.id
}

# Prepare email body
$requestBody = @{ 
    message   = @{
        to      = @{
            emailAddresses = $emailAddresses
            tfIds          = $tfIds
        }
        cc      = @{}
        replyTo = @{}
        body    = "This is a test mail"
        subject = "[Azure DevOps] Test mail"
    }
    projectId = $env:SYSTEM_TEAMPROJECTID
    ids       = @($workItemId)
}

$arguments = @{
    Uri         = ("{0}{1}/_apis/wit/sendmail?api-version=7.0" -f $env:SYSTEM_TEAMFOUNDATIONSERVERURI, $env:SYSTEM_TEAMPROJECT)
    Method      = 'POST' 
    Headers     = $adoHeader 
    Body        = ConvertTo-Json -InputObject $requestBody -Depth 5
    ContentType = 'application/json; charset=utf-8'

}

# Send mail
Invoke-RestMethod @arguments

Links

The title photo is by erica steeves on Unsplash. Thanks.

0
Subscribe to my newsletter

Read articles from Niklas Ohlrogge directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Niklas Ohlrogge
Niklas Ohlrogge