Services/MgGraph.ps1
Function ConvertTo-IMicrosoftGraphRecipient { [CmdletBinding()] param( [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [AllowEmptyCollection()] [AllowNull()] [pscustomobject]$EmailAddress, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string]$Name ) begin { # Return Null If Provided Recipient is Empty if (([string]::IsNullOrEmpty($EmailAddress)) -and ([string]::IsNullOrEmpty($EmailAddress.Address))) { return $null } } process { # Loop through each of the recipient parameter array objects $IMicrosoftGraphRecipient = foreach ($address in $EmailAddress) { # Check if string (email address) or object/hashtable/etc. If not, separate out. if (-not ($address.GetType().Name -eq 'String')) { # Verify object contains 'Address' key or property. if ([string]::IsNullOrEmpty($address.AddressObj)) { throw "Improperly formatted from, recipient, or reply to address." } # Set 'Name' & update 'Address' (do 'Name' 1st!) $Name = $address.Name $address = $address.AddressObj } if ([string]::IsNullOrEmpty($Name)) { @{ EmailAddress = @{Address = $address} } } else { @{ EmailAddress = [ordered]@{ Name = $Name Address = $address} } } } } end { return $IMicrosoftGraphRecipient } } function ConvertTo-IMicrosoftGraphItemBody { [CmdletBinding()] param( [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string]$Content, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateSet('Text','HTML')] [string]$ContentType = 'Text' # The MIME type. See https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/message-format-and-transmission ) begin {} process { $IMicrosoftGraphItemBody = @{ ContentType = $ContentType Content = $Content } return $IMicrosoftGraphItemBody } end {} } Function ConvertTo-IMicrosoftGraphAttachment { [CmdletBinding()] param( [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [AllowNull()] [array]$Attachment ) begin { if ([string]::IsNullOrEmpty($Attachment)) { return $null } } process { [array]$IMicrosoftGraphAttachment = foreach ($currentAttachment in $Attachment) { if (($currentAttachment.ContainsKey('Name')) -and $currentAttachment.ContainsKey('Content')) { $Attachment_ByteEncoded = [System.Convert]::ToBase64String($currentAttachment.Content) [PSCustomObject]$IMicrosoftGraphAttachmentItem = @{ "@odata.type" = "#microsoft.graph.fileAttachment" Name = $currentAttachment.Name ContentBytes = $Attachment_ByteEncoded } $IMicrosoftGraphAttachmentItem } else { throw "The attachment hashtable object is improperly formatted. The hashtable requires the keys of `'Name`' and `'Contents`'" } } } end { return $IMicrosoftGraphAttachment } } Function ConvertTo-IMicrosoftGraphChatMessageAttachment { [CmdletBinding()] param( [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [AllowNull()] [array]$MgDriveItem ) begin { if ([string]::IsNullOrEmpty($MgDriveItem)) { return $null } } process { [array]$IMicrosoftGraphChatMessageAttachment = foreach ($currentAttachment in $MgDriveItem) { if ($currentAttachment.ContainsKey('name') -and $currentAttachment.ContainsKey('webUrl')) { [PSCustomObject]$IMicrosoftGraphChatMessageAttachmentItem = @{ ContentType = 'reference' ContentUrl = $currentAttachment.webUrl Name = $currentAttachment.name } $IMicrosoftGraphChatMessageAttachmentItem } else { throw "The attachment hashtable object is improperly formatted. The hashtable requires the keys of `'webUrl`' and `'name`'" } } } end { return $IMicrosoftGraphChatMessageAttachment } } function ConvertTo-IMicrosoftGraphConversationMember { [CmdletBinding()] param( [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [AllowEmptyCollection()] [AllowNull()] [pscustomobject]$EmailAddress ) begin { # Return Null If Provided Recipient is Empty if ([string]::IsNullOrEmpty($EmailAddress)) { return $null } } process { # Loop through each of the recipient parameter array objects $IMicrosoftGraphRecipient = foreach ($address in $EmailAddress) { # Check if string (email address) or object/hashtable/etc. If not, separate out. if (-not ($address.GetType().Name -eq 'String')) { throw "Improperly formatted from or recipient address." } # Return IMicrosoftGraphConversationMember @{ '@odata.type' = "#microsoft.graph.aadUserConversationMember" roles = @( "owner" ) "user@odata.bind" = "https://graph.microsoft.com/v1.0/users('$address')" } } } end { return $IMicrosoftGraphRecipient } } function ConvertTo-IMicrosoftGraphDriveInvite { [CmdletBinding()] param( [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [AllowEmptyCollection()] [AllowNull()] [pscustomobject]$EmailAddress ) begin { # Return Null If Provided Recipient is Empty if ([string]::IsNullOrEmpty($EmailAddress)) { return $null } } process { # Loop through each of the recipient parameter array objects [array]$IMicrosoftGraphDriveRecipient = foreach ($address in $EmailAddress) { # Check if string (email address) or object/hashtable/etc. If not, separate out. if (-not ($address.GetType().Name -eq 'String')) { throw "Improperly formatted recipient address." } # Return IMicrosoftGraphDriveRecipient @{ email = $address } } $IMicrosoftGraphDriveInvite = @{ recipients = $IMicrosoftGraphDriveRecipient requireSignIn = $true sendInvitation = $false roles = @( "read" ) } } end { return $IMicrosoftGraphDriveInvite } } function Connect-ScriptMessage_MgGraph { [CmdletBinding()] param( [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [pscustomobject]$ServiceConfig ) begin { } process { # Check For Microsoft.Graph Module #TODO: Don't check and import the modules unless needed, depending on allowed services . Chat & Mail # Don't import the entire 'Microsoft.Graph' module. Only import the needed sub-modules. Import-Module 'Microsoft.Graph.Authentication' -ErrorAction SilentlyContinue # Used for Connect-MgGraph, Disconnect-MgGraph, & Get-MgContext. A required module for all Graph modules. Import-Module 'Microsoft.Graph.Users.Actions' -ErrorAction SilentlyContinue # Used for Send-MgUserMail. Import-Module 'Microsoft.Graph.Teams' -ErrorAction SilentlyContinue # Used for New-MgChat & New-MgChatMessage. # If Chat uploads are enabled (based on the config item 'MgDelegatedPermission_RequestFilesReadWritePermission' being set to true), check for the needed module. if ($ServiceConfig.MgDelegatedPermission_RequestFilesReadWritePermission -eq $true) { Import-Module 'Microsoft.Graph.Files' -ErrorAction SilentlyContinue # Used for Get-MgUserDrive # Check for modules. if (!(Get-Module -Name 'Microsoft.Graph.Users.Actions') -or !(Get-Module -Name 'Microsoft.Graph.Teams') -or !(Get-Module -Name 'Microsoft.Graph.Files')) { # Module is not available. Write-Error "Please first install the Microsoft.Graph.Users.Actions, Microsoft.Graph.Teams, & Microsoft.Graph.Files sub-modules from https://www.powershellgallery.com/packages/Microsoft.Graph/ " Return } } else { # Check for modules. if (!(Get-Module -Name 'Microsoft.Graph.Users.Actions') -or !(Get-Module -Name 'Microsoft.Graph.Teams')) { # Module is not available. Write-Error "Please first install the Microsoft.Graph.Users.Actions & Microsoft.Graph.Teams sub-modules from https://www.powershellgallery.com/packages/Microsoft.Graph/ " Return } } # Connect to the Microsoft Graph API. $MgPermissionType = $ServiceConfig.MgPermissionType $MgTenantID = $ServiceConfig.MgTenantID $MgClientID = $ServiceConfig.MgClientID switch ($MgPermissionType) { Delegated { # E.g. Connect-MgGraph -Scopes "User.Read.All","Group.ReadWrite.All" # You can add additional permissions by repeating the Connect-MgGraph command with the new permission scopes. # View the current scopes under which the PowerShell SDK is (trying to) execute cmdlets: Get-MgContext | select -ExpandProperty Scopes # List all the scopes granted on the service principal object (you cn also do it via the Azure AD UI): Get-MgServicePrincipal -Filter "appId eq '14d82eec-204b-4c2f-b7e8-296a70dab67e'" | % { Get-MgServicePrincipalOauth2PermissionGrant -ServicePrincipalId $_.Id } | fl # Find Graph permission needed. More info on permissions: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent) # E.g., Find-MgGraphPermission -SearchString "Teams" -PermissionType Delegated # E.g., Find-MgGraphPermission -SearchString "Teams" -PermissionType Application # The Microsoft Authentication Library (MSAL) currently specifies offline_access, openid, profile, and email by default in authorization and token requests. $MicrosoftGraphScopes = @( 'email' # Allows the app to read your users' primary email address 'offline_access' # With the Microsoft identity platform v2.0 endpoint, you specify the offline_access scope in the scope parameter to explicitly request a refresh token when using the OAuth 2.0 or OpenID Connect protocols. 'openid' # Allows users to sign in to the app with their work or school accounts and allows the app to see basic user profile information. 'profile' # Allows the app to see your users' basic profile (e.g., name, picture, user name, email address) ) if ($ServiceConfig.AllowableMessageTypes -contains 'Mail') { $MicrosoftGraphScopes += @( 'Mail.Send' # With the Mail.Send permission, an app can send mail and save a copy to the user's Sent Items folder, even if the app isn't granted the Mail.ReadWrite or Mail.ReadWrite.Shared permission. # 'Mail.Send.Shared' # This scope doesn't seem to be needed for sending as or on behalf of another user. I wonder if being able to do so using just 'Mail.Send' is a bug... > https://learn.microsoft.com/en-us/graph/outlook-send-mail-from-other-user ) } if ($ServiceConfig.AllowableMessageTypes -contains 'Chat') { $MicrosoftGraphScopes += @( 'Chat.Create' # Allows the app to create chats on behalf of the signed-in user. 'ChatMessage.Send' # Allows an app to send one-to-one and group chat messages in Microsoft Teams, on behalf of the signed-in user. ) if ($ServiceConfig.MgDelegatedPermission_RequestChatReadPermission -eq $true) { $MicrosoftGraphScopes += @( 'Chat.Read' # Allows an app to read 1 on 1 or group chats threads, on behalf of the signed-in user. ) } else { $MicrosoftGraphScopes += @( 'Chat.ReadBasic' # Allows an app to read the members and descriptions of one-to-one and group chat threads, on behalf of the signed-in user. ) } if ($ServiceConfig.MgDelegatedPermission_RequestFilesReadWritePermission -eq $true) { $MicrosoftGraphScopes += @( 'Files.ReadWrite' # Allows the app to read, create, update and delete the signed-in user's files. ) } } $null = Connect-MgGraph -Scopes $MicrosoftGraphScopes -TenantId $MgTenantID -ClientId $MgClientID } Application { [string]$MgApp_AuthenticationType = $ServiceConfig.MgApp_AuthenticationType Write-Verbose -Message "Microsoft Graph App Authentication Type: $MgApp_AuthenticationType" switch ($MgApp_AuthenticationType) { CertificateFile { # This is only supported using PowerShell 7.4 and later because 5.1 is missing the necessary parameters when using 'Get-PfxCertificate'. if ($PSVersionTable.PSVersion -lt [Version]'7.4') { $NewMessage = "Connecting to Microsoft Graph using a certificate file is only supported with PowerShell version 7.4 and later." throw $NewMessage } $MgApp_CertificatePath = $ExecutionContext.InvokeCommand.ExpandString($ServiceConfig.MgApp_CertificatePath) # Try accessing private key certificate without password using current process credentials. [X509Certificate]$MgApp_Certificate = $null try { [X509Certificate]$MgApp_Certificate = Get-PfxCertificate -FilePath $MgApp_CertificatePath -NoPromptForPassword } catch # If that doesn't work try the included credentials. { $MgApp_EncryptedCertificatePassword = $ServiceConfig.MgApp_EncryptedCertificatePassword if ([string]::IsNullOrEmpty($MgApp_EncryptedCertificatePassword)) { $NewMessage = "Cannot access .pfx private key certificate file and no password has been provided." throw $NewMessage } else { [SecureString]$MgApp_EncryptedCertificateSecureString = $MgApp_EncryptedCertificatePassword | ConvertTo-SecureString # Can only be decrypted by the same AD account on the same computer. [X509Certificate]$MgApp_Certificate = Get-PfxCertificate -FilePath $MgApp_CertificatePath -NoPromptForPassword -Password $MgApp_EncryptedCertificateSecureString } } $null = Connect-MgGraph -TenantId $MgTenantID -ClientId $MgClientID -Certificate $MgApp_Certificate } CertificateName { $MgApp_CertificateName = $ServiceConfig.MgApp_CertificateName $null = Connect-MgGraph -TenantId $MgTenantID -ClientId $MgClientID -CertificateName $MgApp_CertificateName } CertificateThumbprint { $MgApp_CertificateThumbprint = $ServiceConfig.MgApp_CertificateThumbprint $null = Connect-MgGraph -TenantId $MgTenantID -ClientId $MgClientID -CertificateThumbprint $MgApp_CertificateThumbprint } ClientSecret { $MgApp_EncryptedSecret = $ServiceConfig.MgApp_EncryptedSecret $ClientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $MgClientID, $($MgApp_EncryptedSecret | ConvertTo-SecureString) $null = Connect-MgGraph -TenantId $MgTenantID -ClientSecretCredential $ClientSecretCredential } Default {throw "Invalid `'MgApp_AuthenticationType`' value."} } } Default {throw "Invalid `'MgPermissionType`' value."} } } end {} } function Disconnect-ScriptMessage_MGGraph { return Disconnect-MgGraph } function Send-ScriptMessage_MgGraph { [CmdletBinding()] param( [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [MessageType[]]$Type, [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [pscustomobject]$From, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [pscustomobject]$ReplyTo, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [pscustomobject]$To, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [pscustomobject]$CC, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [pscustomobject]$BCC, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [bool]$SaveToSentItems = $true, [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string]$Subject, [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [pscustomobject]$Body, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [array]$Attachment, # Array of Content(bytes), File paths, and/or Directory paths [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string]$SenderId, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ChatType]$ChatType, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [bool]$IncludeBCCInGroupChat ) begin { # Set the Service ID. $ServiceId = 'MgGraph' } process { # Send the message on each supported service specified. foreach ($typeItem in $Type) { # Reset Warnings $MgWarningMessages = @() switch ($typeItem) { Mail { # Convert Parameters to IMicrosoft* $Message = @{} $Message['From'] = ConvertTo-IMicrosoftGraphRecipient -EmailAddress $From [array]$Message['ReplyTo'] = ConvertTo-IMicrosoftGraphRecipient -EmailAddress $ReplyTo [array]$Message['To'] = ConvertTo-IMicrosoftGraphRecipient -EmailAddress $To [array]$Message['CC'] = ConvertTo-IMicrosoftGraphRecipient -EmailAddress $CC [array]$Message['BCC'] = ConvertTo-IMicrosoftGraphRecipient -EmailAddress $BCC if (-not [string]::IsNullOrEmpty($Body.Content)) { if ([string]::IsNullOrEmpty($Body.ContentType)) # Don't send 'ContentType' if not provided. It will default to 'Text' { [hashtable]$Message['Body'] = ConvertTo-IMicrosoftGraphItemBody -Content $Body.Content } else { [hashtable]$Message['Body'] = ConvertTo-IMicrosoftGraphItemBody -Content $Body.Content -ContentType $Body.ContentType } } [array]$Message['Attachment'] = ConvertTo-IMicrosoftGraphAttachment -Attachment $Attachment # Build Email $EmailParams = [ordered]@{ SaveToSentItems = $SaveToSentItems Message = [ordered]@{ From = $Message.From ReplyTo = $Message.ReplyTo ToRecipients = $Message.To CcRecipients = $Message.CC BccRecipients = $Message.BCC Subject = $Subject Body = $Message.Body Attachments = $Message.Attachment } } # Check For Separate 'SenderID' Value. Make equal to 'From' if not provided. if ([string]::IsNullOrEmpty($SenderId)) { $SenderId = $Message.From.emailAddress.Address } # Send Email. $SendEmailMessageResult = Send-MgUserMail -UserId $SenderId -BodyParameter $EmailParams -PassThru # Collect Return Info $SendScriptMessageResult_SentFrom = [PSCustomObject]@{ Name = $From.Name Address = $From.AddressObj } [array]$SendScriptMessageResult_Recipients_To = foreach ($i in $To) { [PSCustomObject]@{ Name = $i.Name Address = $i.AddressObj } } [array]$SendScriptMessageResult_Recipients_CC = foreach ($i in $CC) { [PSCustomObject]@{ Name = $i.Name Address = $i.AddressObj } } [array]$SendScriptMessageResult_Recipients_BCC = foreach ($i in $BCC) { [PSCustomObject]@{ Name = $i.Name Address = $i.AddressObj } } [array]$SendScriptMessageResult_Recipients_All = @( # Since Address is also a PSMethod we need to do some fun stuff (List<psobject> doesn't have a method called Address) so we don't get the dreaded 'OverloadDefinitions'. [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_To).Address [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_CC).Address [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_BCC).Address ) [array]$SendScriptMessageResult_Recipients_All = $SendScriptMessageResult_Recipients_All | Sort-Object -Unique # Remove duplicate items. $SendScriptMessageResult_Recipients = [PSCustomObject]@{ To = $SendScriptMessageResult_Recipients_To CC = $SendScriptMessageResult_Recipients_CC BCC = $SendScriptMessageResult_Recipients_BCC All = $SendScriptMessageResult_Recipients_All } $SendScriptMessageResult = [PSCustomObject]@{ MessageService = $ServiceId MessageType = $typeItem Status = $SendEmailMessageResult # The SDK only returns $true and nothing else (and only that because of the 'PassThru') Error = $null SentFrom = $SendScriptMessageResult_SentFrom Recipients = $SendScriptMessageResult_Recipients } # If successful, output result info. $SendScriptMessageResult } Chat{ # TODO MgChat: If application permissions, then do a bot message. Maybe for delegated give option of direct or bot message. # Application CHAT permissions are only supported for migration into a Teams Channel. $ScriptMessageConfig = Get-ScriptMessageConfig if ($ScriptMessageConfig.$ServiceId.MgPermissionType -eq 'Application') { $NewMessage = "Microsoft Graph does not support sending Chat messages using Application permissions. Application permissions are only supported for migration into a Teams Channel." Write-Warning -Message $NewMessage $MgWarningMessages += "$NewMessage" continue } # Grab the latest MgGraph service context. $MgGraphContext = Get-ScriptMessageContext -Service $ServiceId # Check For Separate 'SenderID' Value. Make equal to 'From' if not provided. if ([string]::IsNullOrEmpty($SenderId)) { $SenderId = $From.AddressObj } # Make sure SenderID is equal to From address because Microsoft Graph Chat doesn't support sending on behalf of others. if ($SenderId -ne $From.AddressObj) { $NewMessage = "Microsoft Graph does not support sending Chat messages on behalf of others." Write-Warning -Message $NewMessage $MgWarningMessages += "$NewMessage" continue } # Collect recipient email addresses [array]$ChatRecipients_To = @(foreach ($i in $To.AddressObj){$i}) [array]$ChatRecipients_CC = @(foreach ($i in $CC.AddressObj){$i}) [array]$ChatRecipients_BCC = @(foreach ($i in $BCC.AddressObj){$i}) if (($ChatType -eq [ChatType]'Group') -and ($IncludeBCCInGroupChat -eq $false)) { [array]$ChatRecipients = $ChatRecipients_To + $ChatRecipients_CC } else { [array]$ChatRecipients = $ChatRecipients_To + $ChatRecipients_CC + $ChatRecipients_BCC } # Remove 'SenderID' address if it exists in the recipients list as well as duplicates [array]$ChatRecipients = $ChatRecipients | Sort-Object -Unique | Where-Object {$_ -ne $SenderId} # Collect all chat participants. [array]$AllChatParticipants = [array]$SenderId + [array]$ChatRecipients # Add a warning that BCC recipients (not in Sender, To, or CC) are not included in the group chat. if (($ChatType -eq [ChatType]'Group') -and ($IncludeBCCInGroupChat -eq $false)) { foreach ($chatRecipient_BCC in $ChatRecipients_BCC) { if ($chatRecipient_BCC -notin $AllChatParticipants) { $NewMessage = "The following BCC recipient is not included in the group chat: $chatRecipient_BCC" Write-Warning -Message $NewMessage $MgWarningMessages += "$NewMessage" } } } # Upload and add any attachments, if needed. # TODO: Check for scope permissions. # Cannot use Set-MgDriveItemContent because it forces a filepath to be provided and we want to provide content directly sometimes. if (-not [string]::IsNullOrEmpty($Attachment)) { # Upload the attached file(s) to OneDrive. $MgUserDrive = Get-MgUserDrive -UserId $($MgGraphContext.Account) $TeamsChatFolder = 'root:/Microsoft Teams Chat Files' # Upload files. This method only supports files up to 250 MB in size. For larger files, we would need to implement the "createUploadSession" method. [array]$MgDriveItem = foreach ($attachmentItem in $Attachment) { $MgGraphDriveEndpointUri = 'https://graph.microsoft.com/v1.0/drives/' $AttachmentFileName = $attachmentItem.Name # Get a list of existing files in the Teams Chat Files folder and rename if a file already exists with the same name. $ExistingFiles = (Get-MgDriveItem -DriveId $MgUserDrive.Id -DriveItemId $TeamsChatFolder -ExpandProperty 'Children').Children $FileNameCounter = 0 while ($ExistingFiles.Name -contains $AttachmentFileName) { $FileNameCounter++ $FileBaseName = [System.IO.Path]::GetFileNameWithoutExtension($AttachmentFileName) $FileExtension = [System.IO.Path]::GetExtension($AttachmentFileName) $AttachmentFileName = "{0} {1}{2}" -f ($FileBaseName -replace ' \d+$',''), $FileNameCounter, $FileExtension } # Upload File # TODO: Test weird characters in filename like pound or something $DriveItemId = "$TeamsChatFolder/$($AttachmentFileName):" $InvokeUri = $($MgGraphDriveEndpointUri + $MgUserDrive.Id + '/' + $DriveItemId + '/content') # Output the drive upload result. #Set-MgDriveItemContent -DriveId $MgUserDrive.Id -DriveItemId $DriveItemId -InFile $Attachment[0] # Overwrites file if it exists Invoke-MgGraphRequest -Method PUT -Uri $InvokeUri -Body $attachmentItem.Content -ContentType 'application/octet-stream' # Overwrites file if it exists } # Update the file(s) sharing permissions. $DriveInviteParams = ConvertTo-IMicrosoftGraphDriveInvite -EmailAddress $ChatRecipients foreach ($UploadDriveItemResult in $MgDriveItem) { $DriveInviteResult = Invoke-MgInviteDriveItem -DriveId $MgUserDrive.Id -DriveItemId $UploadDriveItemResult.id -BodyParameter $DriveInviteParams } # Convert Parameters to IMicrosoft* $Message = @{} if (-not [string]::IsNullOrEmpty($Body.Content)) { if ([string]::IsNullOrEmpty($Body.ContentType)) # Don't send 'ContentType' if not provided. It will default to 'Text' { [hashtable]$Message['Body'] = ConvertTo-IMicrosoftGraphItemBody -Content $Body.Content } else { [hashtable]$Message['Body'] = ConvertTo-IMicrosoftGraphItemBody -Content $Body.Content -ContentType $Body.ContentType } } $Message['Attachment'] = [array](ConvertTo-IMicrosoftGraphChatMessageAttachment -MgDriveItem $MgDriveItem) $ChatParams = [ordered]@{ Body = $Message.Body Attachments = $Message.Attachment } } else { $ChatParams = [ordered]@{ Body = $Message.Body } } # Create a new chat object, if needed, & send the message. $Member_SenderID = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $SenderId) switch ($ChatType) { OneOnOne { foreach ($chatRecipient in $ChatRecipients) { $Member_ChatRecipients = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $chatRecipient) [array]$Message['Members'] = [array]$Member_SenderID + [array]$Member_ChatRecipients try { $NewChatResult = New-MgChat -ChatType $ChatType.ToString() -Members $Message.Members $SendChatMessageResult = New-MgChatMessage -ChatId $NewChatResult.Id -BodyParameter $ChatParams } catch { $NewMessage = "Cannot create a chat with the recipient '$($chatRecipient)'." Write-Warning -Message $NewMessage $MgWarningMessages += "$NewMessage" } } } Group #TODO: Sending more than one attachment causes no attachments to be included in the chat message. { # Collect Group Members [array]$Member_ChatRecipients = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $ChatRecipients) [array]$Message['Members'] = [array]$Member_SenderID + [array]$Member_ChatRecipients # See if a group chat already exists with the same recipients. $MGChatProperties = @( 'ChatType', 'Id', 'LastUpdatedDateTime' ) # If the script has 'Chat.Read' or 'Chat.ReadWrite', then sort by the message preview (last time a message was sent). Otherwise, sort by the last time the chat OBJECT was updated. [array]$MicrosoftGraphScopes = $MgGraphContext | Select-Object -ExpandProperty Scopes if (@($MicrosoftGraphScopes) -contains 'Chat.Read' -or @($MicrosoftGraphScopes) -contains 'Chat.ReadWrite') { # It is slower, but we are using the -All parameter so that there is an accurate history of chats. Otherwise, it's possible that we can have multiple groups with the same members from your scripts. $ExistingGroupChats = Get-MgChat -All -Filter "ChatType eq 'group'" -Property $MGChatProperties -ExpandProperty 'Members', "LastMessagePreview" $ExistingGroupChats = $ExistingGroupChats | Sort-Object -Property {$_.LastMessagePreview.CreatedDateTime} -Descending } else # Only has Chat.ReadBasic so we can't see the last message preview. { # It is slower, but we are using the -All parameter so that there is an accurate history of chats. Otherwise, it's possible that we can have multiple groups with the same members from your scripts. $ExistingGroupChats = Get-MgChat -All -Filter "ChatType eq 'group'" -Property $MGChatProperties -ExpandProperty 'Members' $ExistingGroupChats = $ExistingGroupChats | Sort-Object -Property LastUpdatedDateTime -Descending } # Reset the variable and then do a compare\search $LatestExistingGroupChatMatch = $null foreach ($existingGroupChat in $ExistingGroupChats) { if (-not (Compare-Object -ReferenceObject @($existingGroupChat.Members.AdditionalProperties.email) -DifferenceObject $AllChatParticipants)) { $LatestExistingGroupChatMatch = $existingGroupChat } } # Send the chat message; create a new chat group if needed. if (-not $LatestExistingGroupChatMatch) { try { $NewChatResult = New-MgChat -ChatType $ChatType.ToString() -Members $Message.Members $ChatToUse = $NewChatResult $SendChatMessageResult = New-MgChatMessage -ChatId $ChatToUse.Id -BodyParameter $ChatParams } catch { $NewMessage = "Cannot create a new Teams group chat due to at least one recipient of the group: '$($ChatRecipients -join ', ')'." Write-Warning -Message $NewMessage $MgWarningMessages += "$NewMessage" } } else { $ChatToUse = $LatestExistingGroupChatMatch $SendChatMessageResult = New-MgChatMessage -ChatId $ChatToUse.Id -BodyParameter $ChatParams } } } # Collect Return Info $SendScriptMessageResult_SentFrom = [PSCustomObject]@{ Name = $From.Name Address = $From.AddressObj } [array]$SendScriptMessageResult_Recipients_To = foreach ($i in $To) { [PSCustomObject]@{ Name = $i.Name Address = $i.AddressObj } } [array]$SendScriptMessageResult_Recipients_CC = foreach ($i in $CC) { [PSCustomObject]@{ Name = $i.Name Address = $i.AddressObj } } [array]$SendScriptMessageResult_Recipients_BCC = foreach ($i in $BCC) { [PSCustomObject]@{ Name = $i.Name Address = $i.AddressObj } } [array]$SendScriptMessageResult_Recipients_All = @( # Since Address is also a PSMethod we need to do some fun stuff (List<psobject> doesn't have a method called Address) so we don't get the dreaded 'OverloadDefinitions'. [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_To).Address [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_CC).Address [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_BCC).Address ) [array]$SendScriptMessageResult_Recipients_All = $SendScriptMessageResult_Recipients_All | Sort-Object -Unique # Remove duplicate items. [bool]$SendScriptMessageResult_Recipients_IncludeBCCInGroupChat = $IncludeBCCInGroupChat $SendScriptMessageResult_Recipients = [PSCustomObject]@{ To = $SendScriptMessageResult_Recipients_To CC = $SendScriptMessageResult_Recipients_CC BCC = $SendScriptMessageResult_Recipients_BCC All = $SendScriptMessageResult_Recipients_All IncludeBCCInGroupChat = $SendScriptMessageResult_Recipients_IncludeBCCInGroupChat } # Compile Caught Errors and Warnings if ($MgWarningMessages.Count -gt 0) { [array]$SendScriptMessageResult_Error = foreach ($mgWarningMessage in $MgWarningMessages) { [PSCustomObject]@{ Type = 'Warning' Message = $mgWarningMessage } } } else { $SendScriptMessageResult_Error = $null } $SendScriptMessageResult = [PSCustomObject]@{ MessageService = $ServiceId MessageType = $typeItem ChatType = $ChatType Status = $SendChatMessageResult Error = $SendScriptMessageResult_Error SentFrom = $SendScriptMessageResult_SentFrom Recipients = $SendScriptMessageResult_Recipients } # If successful, output result info. $SendScriptMessageResult } Default { Write-Warning -Message "'$($typeItem)' is an invalid message type for service '$($ServiceId)'." } } } } end {} } |