Send Mail with Azure DevOps REST API to a user group


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 <
or >
. 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.
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
API References
Get project descriptor - REST API (Azure DevOps Graph) on Microsoft Learn
Get groups - REST API (Azure DevOps Graph) on Microsoft Learn
Get memberships - REST API (Azure DevOps Graph) on Microsoft Learn
Get identities - REST API (Azure DevOps Identities) on Microsoft Learn
Send Mail - REST API (Azure DevOps Work Item Tracking) on Microsoft Learn
Azure DevOps is not sending emails to nested groups (Scenario 4): How notification email recipients get determined - Azure DevOps on Microsoft Learn
Originally, Rajeev Gupta described 2020 on medium.com how to "Send simple email in Azure DevOps using PowerShell (without any smtp server)". This I used as a starting point.
The title photo is by erica steeves on Unsplash. Thanks.
Subscribe to my newsletter
Read articles from Niklas Ohlrogge directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
