Email users after Cloud PC have been provisioned

By | July 17, 2022

Introduction

Sending the user an email with guidelines on connecting before their Cloud PC is ready could lead the user to try to connect before the Cloud PC is ready. Wouldn’t it be cool if a user only gets an email when their Cloud PC is provisioned and ready to use? in my mind, that is the optimal way of giving the information. I will go through the aspect of how this is working.

How to enable this feature

This isn’t a built-in feature in the Windows 365 Service, so we must use Microsoft Graph to accomplish a solution like this. This solution is based on the Microsoft Graph PowerShell SDK module because dealing with PowerShell is much simpler from my point of view.

Before running the script and sending emails to all users with a Cloud PC, it’s essential to understand some important aspects of the script.

How the script works

It’s not that complicated how the script works. First, it will get all the Cloud PC that is done provisioning. From there, it will look at each Cloud PC Azure AD object and check for a specific extension attribute and value. If the value isn’t there, it will send an email to the primary user of the Cloud PC. At last, it will write the value to the extension attribute so the next time the script runs, it knows the user has an email for that Cloud PC device.

The admin user you execute the script with must have a mailbox as the email will be sent from that user. This is something that will be changed in the upcoming updates.

Modules and API Permissions

The script uses Microsoft Graph PowerShell SDK modules to read and write information. All the required PowerShell modules will automatically be installed once you run the script.

The account you authenticate with has to have the following API permission:

  • CloudPC.Read.All
  • User.Read.All
  • Directory.ReadWrite.All
  • Mail.Send
  • Device.Read.All
  • Directory.AccessAsUser.All

Email content and attachment

The email content and attachment that will be sent must be prepared before running the script.
Only HTML format is currently supported for the email content.

The attachment can be any file type. The max size for an attachment is 4 MB, and only one attachment is currently allowed.

  1. Open Outlook and create a new email
  2. Fill in the content your want
  1. Go to File and save the message as HTML.

Existing Cloud PC devices

As described earlier, the script looks at a specific extension attribute on the AzureAD object. If you already have some users working on their Cloud PC, it might be a good idea to ensure the correct value is present on the existing AzureAD objects so those users won’t get an email.

Fill out the variables in the script below to set it on all your existing Cloud PC devices. Please note that it has to be the same extension attribute and value you use when executing the script for new Cloud PC Devices.

    param(
             #Welcome Email Attribute Check
            [parameter(HelpMessage = "Specify an extension Attribute between 1 - 15 you want the script to use. e.g. extensionAttribute3")]
            [string]$ExstensionAttributeKey = "",

            [parameter(HelpMessage = "Value of exstension Attribute e.g. CPCWelcomeMailHaveBeenSent")]
            [string]$ExstensionAttributeValue = ""
     )

#Function to check if MS.Graph module is installed and up-to-date
function invoke-graphmodule {
    $graphavailable = (find-module -name microsoft.graph)
    $vertemp = $graphavailable.version.ToString()
    Write-Output "Latest version of Microsoft.Graph module is $vertemp" | out-host

    foreach ($module in $modules){
        write-host "Checking module - " $module
        $graphcurrent = (get-installedmodule -name $module -ErrorAction SilentlyContinue)

        if ($graphcurrent -eq $null) {
            write-output "Module is not installed. Installing..." | out-host
            try {
                Install-Module -name $module -Force -ErrorAction Stop 
                Import-Module -name $module -force -ErrorAction Stop 

                }
            catch {
                write-output "Failed to install " $module | out-host
                write-output $_.Exception.Message | out-host
                Return 1
                }
        }
    }


    $graphcurrent = (get-installedmodule -name Microsoft.Graph.DeviceManagement.Functions)
    $vertemp = $graphcurrent.Version.ToString() 
    write-output "Current installed version of Microsoft.Graph module is $vertemp" | out-host

    if ($graphavailable.Version -gt $graphcurrent.Version) { write-host "There is an update to this module available." }
    else
    { write-output "The installed Microsoft.Graph module is up to date." | out-host }
}

function connect-msgraph {

    $tenant = get-mgcontext
    if ($tenant.TenantId -eq $null) {
        write-output "Not connected to MS Graph. Connecting..." | out-host
        try {
            Connect-MgGraph -Scopes $GraphAPIPermissions -ErrorAction Stop | Out-Null
        }
        catch {
            write-output "Failed to connect to MS Graph" | out-host
            write-output $_.Exception.Message | out-host
            Return 1
        }   
    }
    $tenant = get-mgcontext
    $text = "Tenant ID is " + $tenant.TenantId
    Write-Output "Connected to Microsoft Graph" | out-host
    Write-Output $text | out-host
}



#Function to set the profile to beta
function set-profile {
    Write-Output "Setting profile as beta..." | Out-Host
    Select-MgProfile -Name beta
}


$modules = @("Microsoft.Graph.Authentication",
             "Microsoft.Graph.Users.Actions",
             "Microsoft.Graph.DeviceManagement.Administration",
             "Microsoft.Graph.Users",
             "Microsoft.Graph.Identity.DirectoryManagement",
             "Microsoft.Graph.DeviceManagement.Functions"
            )

$WarningPreference = 'SilentlyContinue'

[String]$GraphAPIPermissions = @("CloudPC.Read.All",
                                  "User.Read.all",
                                  "Directory.ReadWrite.All",
                                  "Mail.Send",
                                  "Device.Read.All",
                                  "Directory.AccessAsUser.All"
                                 )

#Commands to load MS.Graph modules
if (invoke-graphmodule -eq 1) {
    write-output "Invoking Graph failed. Exiting..." | out-host
    Return 1
}

#Command to connect to MS.Graph PowerShell app
if (connect-msgraph -eq 1) {
    write-output "Connecting to Graph failed. Exiting..." | out-host
    Return 1
}

set-profile
 
 #Get all provisioned Cloud PC Devices
 
 $AllCPCDevices = Get-MgDevice -Filter "startsWith(Displayname,'CPC-')"
Foreach ($CPCDeviceInfo in $AllCPCDevices){
    #Check if Cloud PC is actived
    if ($CPCDeviceInfo.AccountEnabled -eq $true){

        #Check For if Welcome mail has been sent before
        $Attributecheck = $CPCDeviceInfo.ExtensionAttributes.$ExstensionAttributeKey
        if (!($Attributecheck -eq $ExstensionAttributeValue)){
            


            #Check if Cloud PC is done priovision
            try {
                $ProvisionStatus = Get-MgDeviceManagementVirtualEndpointCloudPC | where-object {$_.ManagedDeviceName -eq $CPCDeviceInfo.DisplayName}
            if ($ProvisionStatus.Status -eq "provisioned") {
                write-host ""
                write-host "Cloud PC: '$($CPCDeviceInfo.DisplayName)' has been provisioned correct and is ready to be logged into."
         
                   try{  
                    #Set Attribute on Azure AD Device
                    Write-Host "Setting Attribute on AzureAD Device:'$($CPCDeviceInfo.DisplayName)'"
                    Write-Host ""
                    $params = @{
                    "extensionAttributes" = @{
                    #Attribute check for if this is a new CloudPC
                    "$ExstensionAttributeKey" = "$ExstensionAttributeValue"
                       }
                     }

                     Update-MgDevice -DeviceId $CPCDeviceInfo.Id -BodyParameter ($params | ConvertTo-Json)
                                            
                      }
                       catch{ 
                             write-output "Unable to set Attribute on AzureAD Device:'$CPCDeviceInfo.DisplayName'" | out-host
                             write-output $_.Exception.Message | out-host
                             break
                            }

            }
                
                }
            
            catch {
                write-output "Unable to get Cloud PC Device status in Endpoint Manager" | out-host
                write-output $_.Exception.Message | out-host
                break
                }


        
        }
 
}

} 

Script execution and result

The script has five parameters to define, all shortly described below. You will find the script with screenshots and results below. In the future, visit my Github to get the latest version of all the scripts, as the scripts in this article won’t be updated.

$ExstensionAttributeKey = Specify which extension attribute the script will use to detect if the email has been sent.
$ExstensionAttributeValue = The value that must exist in the extension attribute.
$MailContentPath = Path to your HTML file that contains your email message.
$EmailAttachment = Path to attachment file. Leave blank if no attachment should be sent.
$EmailSubject = The subject header in the email that is being sent.

 param(
             #Welcome Email Attribute Check
            [parameter(HelpMessage = "Specify an extension Attribute between 1 - 15 you want the script to use. e.g. extensionAttribute3")]
            [string]$ExstensionAttributeKey = "",

            [parameter(HelpMessage = "Value of exstension Attribute e.g. CPCWelcomeMailHaveBeenSent")]
            [string]$ExstensionAttributeValue = "",

            #Mail Contenct path
            [parameter(HelpMessage = "Mail content path e.g. C:\temp\message.html")]
            [string]$MailContentPath = "",
            
            #Email Attachment
            [parameter(HelpMessage = "Leave this blank if no email attachment is required, else specify the location to an attachment. e.g. C:\temp\attachment.pdf")]
            [string]$EmailAttachment = "",
            
            #Send Email Variable
            [parameter(HelpMessage = "Email Subject of the email")]
            [string]$EmailSubject = ""
            
                      

     )

#Function to check if MS.Graph module is installed and up-to-date
function invoke-graphmodule {
    $graphavailable = (find-module -name microsoft.graph)
    $vertemp = $graphavailable.version.ToString()
    Write-Output "Latest version of Microsoft.Graph module is $vertemp" | out-host

    foreach ($module in $modules){
        write-host "Checking module - " $module
        $graphcurrent = (get-installedmodule -name $module -ErrorAction SilentlyContinue)

        if ($graphcurrent -eq $null) {
            write-output "Module is not installed. Installing..." | out-host
            try {
                Install-Module -name $module -Force -ErrorAction Stop 
                Import-Module -name $module -force -ErrorAction Stop 

                }
            catch {
                write-output "Failed to install " $module | out-host
                write-output $_.Exception.Message | out-host
                Return 1
                }
        }
    }


    $graphcurrent = (get-installedmodule -name Microsoft.Graph.DeviceManagement.Functions)
    $vertemp = $graphcurrent.Version.ToString() 
    write-output "Current installed version of Microsoft.Graph module is $vertemp" | out-host

    if ($graphavailable.Version -gt $graphcurrent.Version) { write-host "There is an update to this module available." }
    else
    { write-output "The installed Microsoft.Graph module is up to date." | out-host }
}

function connect-msgraph {

    $tenant = get-mgcontext
    if ($tenant.TenantId -eq $null) {
        write-output "Not connected to MS Graph. Connecting..." | out-host
        try {
            Connect-MgGraph -Scopes $GraphAPIPermissions -ErrorAction Stop | Out-Null
        }
        catch {
            write-output "Failed to connect to MS Graph" | out-host
            write-output $_.Exception.Message | out-host
            Return 1
        }   
    }
    $tenant = get-mgcontext
    $text = "Tenant ID is " + $tenant.TenantId
    Write-Output "Connected to Microsoft Graph" | out-host
    Write-Output $text | out-host
}



#Function to set the profile to beta
function set-profile {
    Write-Output "Setting profile as beta..." | Out-Host
    Select-MgProfile -Name beta
}


$modules = @("Microsoft.Graph.Authentication",
             "Microsoft.Graph.Users.Actions",
             "Microsoft.Graph.DeviceManagement.Administration",
             "Microsoft.Graph.Users",
             "Microsoft.Graph.Identity.DirectoryManagement",
             "Microsoft.Graph.DeviceManagement.Functions"
            )

$WarningPreference = 'SilentlyContinue'

[String]$GraphAPIPermissions = @("CloudPC.Read.All",
                                  "User.Read.all",
                                  "Directory.ReadWrite.All",
                                  "Mail.Send",
                                  "Device.Read.All",
                                  "Directory.AccessAsUser.All"
                                 )

#Commands to load MS.Graph modules
if (invoke-graphmodule -eq 1) {
    write-output "Invoking Graph failed. Exiting..." | out-host
    Return 1
}

#Command to connect to MS.Graph PowerShell app
if (connect-msgraph -eq 1) {
    write-output "Connecting to Graph failed. Exiting..." | out-host
    Return 1
}

set-profile



#Check if Email content is reachable..
write-host "Checking if the email content is reachable..."
try {
write-host "Gathering Email content"
$EmailBody = Get-Content $MailContentPath
$EmailBody  = @"
$EmailBody 
"@
}
catch {
write-output "Failed to get Email content" | out-host
write-output $_.Exception.Message | out-host
break
}


#Get All Cloud PCDevice
$AllCPCDevices = Get-MgDevice -Filter "startsWith(Displayname,'CPC-')"
Foreach ($CPCDeviceInfo in $AllCPCDevices){
    #Check if Cloud PC is actived
    if ($CPCDeviceInfo.AccountEnabled -eq $true){

        #Check For if Welcome mail has been sent before
        $Attributecheck = $CPCDeviceInfo.ExtensionAttributes.$ExstensionAttributeKey
        if (!($Attributecheck -eq $ExstensionAttributeValue)){
            


            #Check if Cloud PC is done priovision
            try {
                $ProvisionStatus = Get-MgDeviceManagementVirtualEndpointCloudPC | where-object {$_.ManagedDeviceName -eq $CPCDeviceInfo.DisplayName}
            if ($ProvisionStatus.Status -eq "provisioned") {
                write-host ""
                write-host "Cloud PC: '$($CPCDeviceInfo.DisplayName)' has been provisioned correct and is ready to be logged into."
            


                      #Gathering user information
                        write-host "Gathering User information"
                        try {
                            $UserID = Get-MgDeviceManagementVirtualEndpointCloudPC | where-object {$_.ManagedDeviceName -eq $CPCDeviceInfo.DisplayName}
                            write-host "Cloud PC: '$($CPCDeviceInfo.DisplayName)' Primary user is: '$($UserID.UserPrincipalName)'"
                            write-host "Finding Email Address for user: '$($UserID.UserPrincipalName)'"
                            #$UserInformation = Get-AzureADUser -Filter "userPrincipalName eq '$($UserID.UserPrincipalName)'"
                            $UserInformation = Get-MgUser -Filter "userPrincipalName eq '$($UserID.UserPrincipalName)'"
                            $PrimarySMTP = $UserInformation.ProxyAddresses -clike 'SMTP:*' -split ":"
                            $PrimarySMTP = $PrimarySMTP[1]
                            write-host "Primary SMTP for user '$($UserInformation.UserPrincipalName)' is: '$PrimarySMTP'"
                            }
                        catch {
                            write-output "Unable to get user information" | out-host
                            write-output $_.Exception.Message | out-host
                            break
                            }


                                  #Send email
                                 
                                 #Get UserID from admin
                                 $EmailUserDetails = Get-MgContext
                                 $EmailUserDetails = Get-MgUser -Filter "userPrincipalName eq '$($EmailUserDetails.Account)'"
                        
                                 #Get File Name and Base64 string
                                if($EmailAttachment){
                             
                                $AttachmentFileName = ( Get-Item -Path $EmailAttachment).name
                                $base64string = [Convert]::ToBase64String([IO.File]::ReadAllBytes($EmailAttachment))
                                 $params = @{
	                                Message = @{
		                                Subject = $EmailSubject
		                                Body = @{
			                                ContentType = "HTML"
			                                Content = $EmailBody
		                                }
		                                ToRecipients = @(
			                                @{
				                                EmailAddress = @{
					                                Address = $PrimarySMTP
				                                }
			                                }
		                                )
		                                Attachments = @(
			                                @{
				                                "@odata.type" = "#microsoft.graph.fileAttachment"
				                                Name = "$AttachmentFileName"
                                                ContentType = "text/plain"
				                                ContentBytes = $base64string
              
			                                }
            
            
		                                )
        
	                                }
	                                SaveToSentItems = "false"
                                 }
                                
                                }else{
                                
                                     $params = @{
	                                Message = @{
		                                Subject = $EmailSubject
		                                Body = @{
			                                ContentType = "HTML"
			                                Content = $EmailBody
		                                }
		                                ToRecipients = @(
			                                @{
				                                EmailAddress = @{
					                                Address = $PrimarySMTP
				                                }
			                                }
		                                )
        
	                                }
	                                SaveToSentItems = "false"
                                }
                            }
                           
                            Send-MgUserMail -UserId $EmailUserDetails.Id -BodyParameter $params
                                
     
                                            try{  
                                                    #Set Attribute on Azure AD Device
                                                    Write-Host "Setting Attribute on AzureAD Device:'$($CPCDeviceInfo.DisplayName)'"
                                                    Write-Host ""
                                                    $params = @{
                                                    "extensionAttributes" = @{
                                                      #Attribute check for if this is a new CloudPC
                                                      "$ExstensionAttributeKey" = "$ExstensionAttributeValue"
                                                       }
                                                    }

                                                             Update-MgDevice -DeviceId $CPCDeviceInfo.Id -BodyParameter ($params | ConvertTo-Json)
                                            
                                            }
                                            
                                            catch{ 
                                            write-output "Unable to set Attribute on AzureAD Device:'$CPCDeviceInfo.DisplayName'" | out-host
                                            write-output $_.Exception.Message | out-host
                                            break

                                            }
            

            }
                
                }
            
            catch {
                write-output "Unable to get Cloud PC Device status in Endpoint Manager" | out-host
                write-output $_.Exception.Message | out-host
                break
                }


        
        }
        
        

}


}

Create an automated flow

Having to run the script manually is not optimal. Therefore it is always a good idea to automate tasks like these. The script must be modified to run in an automated task on a server. With the proper knowledge, it can be done. But wouldn’t it be cool if this could be automated with modern services in Microsoft Azure?

I’m working with my good friend Michael Mardahl on a cloud-based solution in Microsoft Azure that can support this automated flow with Windows 365. Keep an eye on the W365community Youtube Channel or follow us on social media to get updated once it is ready.

Final Thoughts

Even though the current solution has to be executed in hand, sending out information to the user post-provisioning of their Cloud PC may remove some confusion and helpdesk requests. It may also simplify the current communication flow from the IT department. I’m looking forward to sharing an automated solution with Michael Mardahl soon.

Leave a Reply

Your email address will not be published.