From cc2361172e4067cc9e83c68a63e0093fccf4ef89 Mon Sep 17 00:00:00 2001 From: Sekers <46898253+Sekers@users.noreply.github.com> Date: Thu, 18 Sep 2025 17:40:40 -0500 Subject: [PATCH 01/15] LIST OBJECTS DON'T SUPPORT NULL I check for null before sending to list objects in case TO, CC, & BCC are empty. I also started prepping for some other enhancements and fixes regarding chat. --- README.md | 4 +- ScriptMessage/Services/MgGraph.ps1 | 313 ++++++++++++++++------------- 2 files changed, 173 insertions(+), 144 deletions(-) diff --git a/README.md b/README.md index e8d5806..fef1de9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ - [This Module is in Public Preview](#this-module-is-in-public-preview) - [Overview](#overview) -- [Currently Supported Services](#currently-supported-services) +- [Supported Services](#supported-services) - [What's New](#whats-new) - [Documentation](#documentation) - [Developing and Contributing](#developing-and-contributing) @@ -23,7 +23,7 @@ ScriptMessage is designed to simplify the use of messaging services in PowerShel - Specify more than one service in a single command to easily send the same message multiple ways for redundancy or other purposes. For example, you might want to send an email using Microsoft Graph and a chat message using both Teams & Slack for the same alert. - Easily switch the desired messaging service(s) in your scripts by updating simple config files. Whether your current messaging service is deprecated, you need to add a new service, or switch to a different service, you will no longer need to rewrite all of your scripts. -## Currently Supported Services +## Supported Services - [**Microsoft Graph SDK PowerShell:**](https://learn.microsoft.com/en-us/powershell/microsoftgraph/overview?view=graph-powershell-1.0) Take advantage of the Microsoft Graph SDK PowerShell module to send email and chat messages using the Graph API without having to learn all the object formatting that the API requires (which unfortunately the SDK doesn't simplify). - Since the Microsoft Graph API only supports Teams Chat when using delegated [permissions](https://learn.microsoft.com/en-us/graph/permissions-overview), we are looking into [Teams Bots](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/overview) support for future releases to allow for application permissions. diff --git a/ScriptMessage/Services/MgGraph.ps1 b/ScriptMessage/Services/MgGraph.ps1 index 29c1d32..d8d27dd 100644 --- a/ScriptMessage/Services/MgGraph.ps1 +++ b/ScriptMessage/Services/MgGraph.ps1 @@ -642,9 +642,18 @@ function Send-ScriptMessage_MgGraph } } [array]$SendScriptMessageResult_Recipients_All = @( # Since Address is also a PSMethod we need to do some fun stuff (List 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 + if ($null -ne $SendScriptMessageResult_Recipients_To) + { + [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_To).Address + } + if ($null -ne $SendScriptMessageResult_Recipients_CC) + { + [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_CC).Address + } + if ($null -ne $SendScriptMessageResult_Recipients_BCC) + { + [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]@{ @@ -671,10 +680,10 @@ function Send-ScriptMessage_MgGraph $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." + $NewMessage = "Chat not sent. 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 + continue #TODO: Remove this continue so we can get the warning message in the output object. See what we may have to change below (probably skip the section). } # Grab the latest MgGraph service context. @@ -689,10 +698,10 @@ function Send-ScriptMessage_MgGraph # 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." + $NewMessage = "Chat not sent. Microsoft Graph does not support sending Chat messages on behalf of others." Write-Warning -Message $NewMessage $MgWarningMessages += "$NewMessage" - continue + continue #TODO: Remove this continue so we can get the warning message in the output object. See what we may have to change below (probably skip the section). } # Collect recipient email addresses @@ -714,180 +723,191 @@ function Send-ScriptMessage_MgGraph $ChatRecipients_BCC } - # Remove 'SenderID' address if it exists in the recipients list as well as duplicates + # Remove 'SenderID' address if it exists in the recipients list as well as duplicates. + # (Graph does not support sending direct chat messages to yourself since that's not a standard chat thread. I think it's some sort of "note" when used by Teams.) [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)) + # Process chat only there are recipients. Otherwise warn if no chat recipients + if ($ChatRecipients.Count -eq 0) + { + $NewMessage = "Chat not sent. No chat recipients exist. If you are trying to send a chat message to yourself, please note that Microsoft doesn't support direct messaging to yourself via the Graph API." + Write-Warning -Message $NewMessage + $MgWarningMessages += "$NewMessage" + } + else { - foreach ($chatRecipient_BCC in $ChatRecipients_BCC) + # 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)) { - if ($chatRecipient_BCC -notin $AllChatParticipants) + foreach ($chatRecipient_BCC in $ChatRecipients_BCC) { - $NewMessage = "The following BCC recipient is not included in the group chat: $chatRecipient_BCC" - Write-Warning -Message $NewMessage - $MgWarningMessages += "$NewMessage" + 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) + # 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)) { - $MgGraphDriveEndpointUri = 'https://graph.microsoft.com/v1.0/drives/' - $AttachmentFileName = $attachmentItem.Name + # 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 + } - # 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 + # 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 } - # 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) + # Convert Parameters to IMicrosoft* + $Message = @{} + if (-not [string]::IsNullOrEmpty($Body.Content)) { - $DriveInviteResult = Invoke-MgInviteDriveItem -DriveId $MgUserDrive.Id -DriveItemId $UploadDriveItemResult.id -BodyParameter $DriveInviteParams + 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) - # 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 + $ChatParams = [ordered]@{ + Body = $Message.Body + Attachments = $Message.Attachment + } } 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 + $ChatParams = [ordered]@{ + Body = $Message.Body + } } - } - # Create a new chat object, if needed, & send the message. - $Member_SenderID = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $SenderId) + # Create a new chat object, if needed, & send the message. + $Member_SenderID = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $SenderId) - switch ($ChatType) - { - OneOnOne + switch ($ChatType) { - foreach ($chatRecipient in $ChatRecipients) + 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. { - $Member_ChatRecipients = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $chatRecipient) + # Collect Group Members + [array]$Member_ChatRecipients = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $ChatRecipients) [array]$Message['Members'] = [array]$Member_SenderID + [array]$Member_ChatRecipients - try + + # 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') { - $NewChatResult = New-MgChat -ChatType $ChatType.ToString() -Members $Message.Members - $SendChatMessageResult = New-MgChatMessage -ChatId $NewChatResult.Id -BodyParameter $ChatParams + # 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 } - catch + else # Only has Chat.ReadBasic so we can't see the last message preview. { - $NewMessage = "Cannot create a chat with the recipient '$($chatRecipient)'." - Write-Warning -Message $NewMessage - $MgWarningMessages += "$NewMessage" + # 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 } - } - } - 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)) + + # Reset the variable and then do a compare\search + $LatestExistingGroupChatMatch = $null + foreach ($existingGroupChat in $ExistingGroupChats) { - $LatestExistingGroupChatMatch = $existingGroupChat + 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 + # Send the chat message; create a new chat group if needed. + if (-not $LatestExistingGroupChatMatch) { - $NewChatResult = New-MgChat -ChatType $ChatType.ToString() -Members $Message.Members - $ChatToUse = $NewChatResult - $SendChatMessageResult = New-MgChatMessage -ChatId $ChatToUse.Id -BodyParameter $ChatParams + 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" + } } - catch + else { - $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" + $ChatToUse = $LatestExistingGroupChatMatch + $SendChatMessageResult = New-MgChatMessage -ChatId $ChatToUse.Id -BodyParameter $ChatParams } } - else - { - $ChatToUse = $LatestExistingGroupChatMatch - $SendChatMessageResult = New-MgChatMessage -ChatId $ChatToUse.Id -BodyParameter $ChatParams - } } } - # Collect Return Info + # Collect Return Info # TODO: How does this work if we have both chat and email service types. I also think that the email type looks different (better) when CC and BBC are missing, etc. $SendScriptMessageResult_SentFrom = [PSCustomObject]@{ Name = $From.Name Address = $From.AddressObj @@ -914,9 +934,18 @@ function Send-ScriptMessage_MgGraph } } [array]$SendScriptMessageResult_Recipients_All = @( # Since Address is also a PSMethod we need to do some fun stuff (List 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 + if ($null -ne $SendScriptMessageResult_Recipients_To) + { + [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_To).Address + } + if ($null -ne $SendScriptMessageResult_Recipients_CC) + { + [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_CC).Address + } + if ($null -ne $SendScriptMessageResult_Recipients_BCC) + { + [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 From 267f3c718a570e3031726e4521a70dd01b78c433 Mon Sep 17 00:00:00 2001 From: Sekers <46898253+Sekers@users.noreply.github.com> Date: Wed, 15 Oct 2025 13:09:52 -0500 Subject: [PATCH 02/15] REVERTED BEGIN|PROCESS|END DUE TO BUGS INTRODUCED. ALSO BEGAN ADDING MAILTYPE. There are some other improvements to chat handling for MGGraph. Plus a couple of bugfixes where the wrong variable "$address" was used instead of "$addressobj". --- ...onvertTo-ScriptMessageAttachmentObject.ps1 | 109 +- .../ConvertTo-ScriptMessageBodyObject.ps1 | 37 +- ...ConvertTo-ScriptMessageRecipientObject.ps1 | 48 +- .../Public/Connect-ScriptMessage.ps1 | 39 +- .../Public/Disconnect-ScriptMessage.ps1 | 66 +- .../Public/Get-ScriptMessageConfig.ps1 | 33 +- .../Public/Get-ScriptMessageContext.ps1 | 75 +- ScriptMessage/Public/Send-ScriptMessage.ps1 | 160 +-- .../Set-ScriptMessageConfigFilePath.ps1 | 9 +- ScriptMessage/ScriptMessage.psm1 | 7 + ScriptMessage/Services/MgGraph.ps1 | 1108 ++++++++--------- 11 files changed, 784 insertions(+), 907 deletions(-) diff --git a/ScriptMessage/Private/ConvertTo-ScriptMessageAttachmentObject.ps1 b/ScriptMessage/Private/ConvertTo-ScriptMessageAttachmentObject.ps1 index 0eedb1d..e125b4f 100644 --- a/ScriptMessage/Private/ConvertTo-ScriptMessageAttachmentObject.ps1 +++ b/ScriptMessage/Private/ConvertTo-ScriptMessageAttachmentObject.ps1 @@ -10,73 +10,64 @@ Function ConvertTo-ScriptMessageAttachmentObject [array]$Attachment ) - begin + if ([string]::IsNullOrEmpty($Attachment)) { - if ([string]::IsNullOrEmpty($Attachment)) - { - return $null - } + return $null } - - process - { - [array]$ScriptMessageAttachment = foreach ($currentAttachment in $Attachment) - { - switch ($currentAttachment.GetType().Name) - { - 'Hashtable' { # If direct file content is supplied. - $AttachmentType = 'Content' - if (($currentAttachment.ContainsKey('Name')) -and $currentAttachment.ContainsKey('Content')) - { - [PSCustomObject]$ScriptMessageAttachmentItem = @{ - Name = $currentAttachment.Name - Content = $currentAttachment.Content - } - } - else - { - throw "The attachment hashtable object is improperly formatted. The hashtable requires the keys of `'Name`' and `'Content`'" + + [array]$ScriptMessageAttachment = foreach ($currentAttachment in $Attachment) + { + switch ($currentAttachment.GetType().Name) + { + 'Hashtable' { # If direct file content is supplied. + $AttachmentType = 'Content' + if (($currentAttachment.ContainsKey('Name')) -and $currentAttachment.ContainsKey('Content')) + { + [PSCustomObject]$ScriptMessageAttachmentItem = @{ + Name = $currentAttachment.Name + Content = $currentAttachment.Content } } - 'String' { # If a directory or file path is supplied. - if (-not (Test-Path -Path $currentAttachment)) - { - throw 'Invalid path to attachment directory or file.' + else + { + throw "The attachment hashtable object is improperly formatted. The hashtable requires the keys of `'Name`' and `'Content`'" + } + } + 'String' { # If a directory or file path is supplied. + if (-not (Test-Path -Path $currentAttachment)) + { + throw 'Invalid path to attachment directory or file.' + } + + switch ((Get-Item -Path $currentAttachment).GetType().Name) + { + 'FileInfo'{ + $AttachmentType = 'FilePath' + $FileInfo = Get-Item -Path $currentAttachment + [PSCustomObject]$ScriptMessageAttachmentItem = @{ + Name = $FileInfo.Name + Content = [System.IO.File]::ReadAllBytes($FileInfo.FullName) + } } - - switch ((Get-Item -Path $currentAttachment).GetType().Name) - { - 'FileInfo'{ - $AttachmentType = 'FilePath' - $FileInfo = Get-Item -Path $currentAttachment - [PSCustomObject]$ScriptMessageAttachmentItem = @{ - Name = $FileInfo.Name - Content = [System.IO.File]::ReadAllBytes($FileInfo.FullName) + 'DirectoryInfo' { + $AttachmentType = 'DirectoryPath' + $DirectoryContent = Get-ChildItem $currentAttachment -File -Recurse + [PSCustomObject]$ScriptMessageAttachmentItem = foreach ($file in $DirectoryContent) + { + @{ + Name = $file.Name + Content = [System.IO.File]::ReadAllBytes($file.FullName) } } - 'DirectoryInfo' { - $AttachmentType = 'DirectoryPath' - $DirectoryContent = Get-ChildItem $currentAttachment -File -Recurse - [PSCustomObject]$ScriptMessageAttachmentItem = foreach ($file in $DirectoryContent) - { - @{ - Name = $file.Name - Content = [System.IO.File]::ReadAllBytes($file.FullName) - } - } - } - Default {throw 'Unexpected attachment object type.'} } + Default {throw 'Unexpected attachment object type.'} } - Default {throw 'Unexpected attachment object type.'} } - - $ScriptMessageAttachmentItem - } - } - - end - { - return $ScriptMessageAttachment - } + Default {throw 'Unexpected attachment object type.'} + } + + $ScriptMessageAttachmentItem + } + + return $ScriptMessageAttachment } \ No newline at end of file diff --git a/ScriptMessage/Private/ConvertTo-ScriptMessageBodyObject.ps1 b/ScriptMessage/Private/ConvertTo-ScriptMessageBodyObject.ps1 index 587a43d..cc5f927 100644 --- a/ScriptMessage/Private/ConvertTo-ScriptMessageBodyObject.ps1 +++ b/ScriptMessage/Private/ConvertTo-ScriptMessageBodyObject.ps1 @@ -11,35 +11,26 @@ function ConvertTo-ScriptMessageBodyObject [pscustomobject]$Body ) - begin + if ([string]::IsNullOrEmpty($Body)) { - if ([string]::IsNullOrEmpty($Body)) - { - return $null - } + return $null } - process + # Check if 'Body' is string. If it is, turn into a PSobject. + if ($Body.GetType().Name -eq 'String') { - # Check if 'Body' is string. If it is, turn into a PSobject. - if ($Body.GetType().Name -eq 'String') - { - $ScriptMessageBodyObject = [PSCustomObject]@{ - ContentType = 'Text' - Content = $Body - } - } - else # Return item as properly formatted PSObject that includes the 'ContentType' property. - { - $ScriptMessageBodyObject = [PSCustomObject]@{ - ContentType = $Body.ContentType - Content = $Body.Content - } + $ScriptMessageBodyObject = [PSCustomObject]@{ + ContentType = 'Text' + Content = $Body } } - - end + else # Return item as properly formatted PSObject that includes the 'ContentType' property. { - return $ScriptMessageBodyObject + $ScriptMessageBodyObject = [PSCustomObject]@{ + ContentType = $Body.ContentType + Content = $Body.Content + } } + + return $ScriptMessageBodyObject } \ No newline at end of file diff --git a/ScriptMessage/Private/ConvertTo-ScriptMessageRecipientObject.ps1 b/ScriptMessage/Private/ConvertTo-ScriptMessageRecipientObject.ps1 index 3107ba1..85f8ad7 100644 --- a/ScriptMessage/Private/ConvertTo-ScriptMessageRecipientObject.ps1 +++ b/ScriptMessage/Private/ConvertTo-ScriptMessageRecipientObject.ps1 @@ -11,47 +11,37 @@ function ConvertTo-ScriptMessageRecipientObject [pscustomobject]$Recipient ) - - begin + if (([string]::IsNullOrEmpty($Recipient)) -and ($Recipient.Count -lt 1)) { - if (([string]::IsNullOrEmpty($Recipient)) -and ($Recipient.Count -lt 1)) - { - return $null - } + return $null } - process + [array]$ScriptMessageRecipientObject = foreach ($recipientItem in $Recipient) { - [array]$ScriptMessageRecipientObject = foreach ($recipientItem in $Recipient) + # Check if 'recipientItem' is string (email address, etc.). If it is, turn into a PSobject. + if ($recipientItem.GetType().Name -eq 'String') + { + [PSCustomObject]@{ + AddressObj = $recipientItem # Don't use 'Address' because it can conflict with the 'Address()' method. + } + } + else # Return item as properly formatted PSObject that includes the 'Name' property. { - # Check if 'recipientItem' is string (email address, etc.). If it is, turn into a PSobject. - if ($recipientItem.GetType().Name -eq 'String') + if ([string]::IsNullOrEmpty($recipientItem.Name)) { [PSCustomObject]@{ - AddressObj = $recipientItem # Don't use 'Address' because it can conflict with the 'Address()' method. + AddressObj = $recipientItem.Address # Don't use 'Address' because it can conflict with the 'Address()' method. } } - else # Return item as properly formatted PSObject that includes the 'Name' property. + else { - if ([string]::IsNullOrEmpty($recipientItem.Name)) - { - [PSCustomObject]@{ - AddressObj = $recipientItem.Address # Don't use 'Address' because it can conflict with the 'Address()' method. - } - } - else - { - [PSCustomObject]@{ - Name = $recipientItem.Name - AddressObj = $recipientItem.Address # Don't use 'Address' because it can conflict with the 'Address()' method. - } + [PSCustomObject]@{ + Name = $recipientItem.Name + AddressObj = $recipientItem.Address # Don't use 'Address' because it can conflict with the 'Address()' method. } } } } - - end - { - return $ScriptMessageRecipientObject - } + + return $ScriptMessageRecipientObject } \ No newline at end of file diff --git a/ScriptMessage/Public/Connect-ScriptMessage.ps1 b/ScriptMessage/Public/Connect-ScriptMessage.ps1 index 17568e0..b0aad2d 100644 --- a/ScriptMessage/Public/Connect-ScriptMessage.ps1 +++ b/ScriptMessage/Public/Connect-ScriptMessage.ps1 @@ -37,32 +37,23 @@ function Connect-ScriptMessage [switch]$ReturnConnectionInfo ) - begin - { - # Set the necessary configuration variables. - $ScriptMessageConfig = Get-ScriptMessageConfig - - # Set the connection parameters. - $ConnectionParameters = @{ - ServiceConfig = $ScriptMessageConfig.$Service - } + # Set the necessary configuration variables. + $ScriptMessageConfig = Get-ScriptMessageConfig + + # Set the connection parameters. + $ConnectionParameters = @{ + ServiceConfig = $ScriptMessageConfig.$Service } - - process #TODO: Can this check if already connected??? + + # Connect to the proper service. + switch ($Service) { - # Connect to the proper service. - switch ($Service) - { - MgGraph {Connect-ScriptMessage_MGGraph @ConnectionParameters} - } + MgGraph {Connect-ScriptMessage_MGGraph @ConnectionParameters} } - - end - { - # Return the connection information, if requested. - if ($ReturnConnectionInfo) - { - return Get-ScriptMessageContext -Service $Service - } + + # Return the connection information, if requested. + if ($ReturnConnectionInfo) + { + return Get-ScriptMessageContext -Service $Service } } diff --git a/ScriptMessage/Public/Disconnect-ScriptMessage.ps1 b/ScriptMessage/Public/Disconnect-ScriptMessage.ps1 index 46d9d02..195ce0e 100644 --- a/ScriptMessage/Public/Disconnect-ScriptMessage.ps1 +++ b/ScriptMessage/Public/Disconnect-ScriptMessage.ps1 @@ -37,55 +37,45 @@ function Disconnect-ScriptMessage [switch]$ReturnConnectionInfo ) - begin + # Disconnect from the proper service. + $ServiceDisconnectReturnInfo = switch ($Service) { + MgGraph {Disconnect-ScriptMessage_MGGraph} } - process - { - # Disconnect from the proper service. - $ServiceDisconnectReturnInfo = switch ($Service) + # Return the disconnection information, if requested. + if ($ReturnConnectionInfo) + { + # Create the disconnect info object to return. + $ScriptMessageDisconnectReturnInfo = New-Object System.Object + + # Retrieve any common disconnection info across services. + $CommonConnectionInfo = [pscustomobject]@{ + Service = $Service.ToString() + } + foreach ($infoItem in $($CommonConnectionInfo.PSObject.Properties)) { - MgGraph {Disconnect-ScriptMessage_MGGraph} + $ScriptMessageDisconnectReturnInfo | Add-Member -MemberType NoteProperty -Name "$($infoItem.Name)" -Value $($infoItem.Value) } - # Return the disconnection information, if requested. - if ($ReturnConnectionInfo) - { - # Create the disconnect info object to return. - $ScriptMessageDisconnectReturnInfo = New-Object System.Object - - # Retrieve any common disconnection info across services. - $CommonConnectionInfo = [pscustomobject]@{ - Service = $Service.ToString() - } - foreach ($infoItem in $($CommonConnectionInfo.PSObject.Properties)) - { - $ScriptMessageDisconnectReturnInfo | Add-Member -MemberType NoteProperty -Name "$($infoItem.Name)" -Value $($infoItem.Value) - } - - # Add in disconnection information. - switch ($Service) - { - MgGraph { - if ([string]::IsNullOrEmpty($ServiceDisconnectReturnInfo)) - { - break # Terminate the switch statement. - } - foreach ($infoItem in $($ServiceDisconnectReturnInfo.PSObject.Properties)) - { - $ScriptMessageDisconnectReturnInfo | Add-Member -MemberType NoteProperty -Name "$($infoItem.Name)" -Value $($infoItem.Value) - } + # Add in disconnection information. + switch ($Service) + { + MgGraph { + if ([string]::IsNullOrEmpty($ServiceDisconnectReturnInfo)) + { + break # Terminate the switch statement. + } + foreach ($infoItem in $($ServiceDisconnectReturnInfo.PSObject.Properties)) + { + $ScriptMessageDisconnectReturnInfo | Add-Member -MemberType NoteProperty -Name "$($infoItem.Name)" -Value $($infoItem.Value) } } } } - end + if ($ReturnConnectionInfo) { - if ($ReturnConnectionInfo) - { - return $ScriptMessageDisconnectReturnInfo - } + return $ScriptMessageDisconnectReturnInfo } } diff --git a/ScriptMessage/Public/Get-ScriptMessageConfig.ps1 b/ScriptMessage/Public/Get-ScriptMessageConfig.ps1 index ed76196..1cad342 100644 --- a/ScriptMessage/Public/Get-ScriptMessageConfig.ps1 +++ b/ScriptMessage/Public/Get-ScriptMessageConfig.ps1 @@ -28,27 +28,20 @@ function Get-ScriptMessageConfig [string]$Path = $ScriptMessage_Global_ConfigFilePath # If not entered will see if it can pull path from this variable. ) - begin {} - - process + # Make Sure Requested Path Isn't Null or Empty (better to catch it here than validating on the parameter of this function) + if ([string]::IsNullOrEmpty($Path)) { - # Make Sure Requested Path Isn't Null or Empty (better to catch it here than validating on the parameter of this function) - if ([string]::IsNullOrEmpty($Path)) - { - throw "`'`$ScriptMessage_Global_ConfigFilePath`' is not specified. Don't forget to first use the `'Set-ScriptMessageConfigFilePath`' cmdlet!" - } - - # Get Config and Secrets - try - { - $ScriptMessageConfig = Get-Content -Path "$Path" -ErrorAction 'Stop' | ConvertFrom-Json - return $ScriptMessageConfig - } - catch - { - throw "Can't find the JSON configuration file. Use 'Set-ScriptMessageConfigFilePath' to create one." - } + throw "`'`$ScriptMessage_Global_ConfigFilePath`' is not specified. Don't forget to first use the `'Set-ScriptMessageConfigFilePath`' cmdlet!" } - end {} + # Get Config and Secrets + try + { + $ScriptMessageConfig = Get-Content -Path "$Path" -ErrorAction 'Stop' | ConvertFrom-Json + return $ScriptMessageConfig + } + catch + { + throw "Can't find the JSON configuration file. Use 'Set-ScriptMessageConfigFilePath' to create one." + } } \ No newline at end of file diff --git a/ScriptMessage/Public/Get-ScriptMessageContext.ps1 b/ScriptMessage/Public/Get-ScriptMessageContext.ps1 index ff70ca2..4076721 100644 --- a/ScriptMessage/Public/Get-ScriptMessageContext.ps1 +++ b/ScriptMessage/Public/Get-ScriptMessageContext.ps1 @@ -38,58 +38,49 @@ function Get-ScriptMessageContext [Switch]$ReturnCachedContext ) - begin - { - # Create the context object to return. - $ScriptMessageContext = New-Object System.Object + # Create the context object to return. + $ScriptMessageContext = New-Object System.Object - # Enable a force refresh if no data exists for the specified service or if $ReturnCachedContext is not present. - if (($null -eq $ScriptMessage_Global_CachedServiceContext.$Service) -or (-not $ReturnCachedContext.IsPresent)) - { - $RefreshContext = $true - } + # Enable a force refresh if no data exists for the specified service or if $ReturnCachedContext is not present. + if (($null -eq $ScriptMessage_Global_CachedServiceContext.$Service) -or (-not $ReturnCachedContext.IsPresent)) + { + $RefreshContext = $true } - process + if ($RefreshContext) { - if ($RefreshContext) + # Retrieve any common connection info across services. + $CommonConnectionInfo = [pscustomobject]@{ + Service = $Service.ToString() + } + foreach ($infoItem in $($CommonConnectionInfo.PSObject.Properties)) { - # Retrieve any common connection info across services. - $CommonConnectionInfo = [pscustomobject]@{ - Service = $Service.ToString() - } - foreach ($infoItem in $($CommonConnectionInfo.PSObject.Properties)) - { - $ScriptMessageContext | Add-Member -MemberType NoteProperty -Name "$($infoItem.Name)" -Value $($infoItem.Value) - } + $ScriptMessageContext | Add-Member -MemberType NoteProperty -Name "$($infoItem.Name)" -Value $($infoItem.Value) + } - # Retrieve connection information. - switch ($Service) - { - MgGraph { - $MgContext = Get-MgContext - if ([string]::IsNullOrEmpty($MgContext)) - { - break # Terminate the switch statement. - } - foreach ($infoItem in $($MgContext.PSObject.Properties)) - { - $ScriptMessageContext | Add-Member -MemberType NoteProperty -Name "$($infoItem.Name)" -Value $($infoItem.Value) - } + # Retrieve connection information. + switch ($Service) + { + MgGraph { + $MgContext = Get-MgContext + if ([string]::IsNullOrEmpty($MgContext)) + { + break # Terminate the switch statement. + } + foreach ($infoItem in $($MgContext.PSObject.Properties)) + { + $ScriptMessageContext | Add-Member -MemberType NoteProperty -Name "$($infoItem.Name)" -Value $($infoItem.Value) } } - - # Update cached context data for the specified service. - $ScriptMessage_Global_CachedServiceContext | Add-Member -MemberType NoteProperty -Name $Service -Value $ScriptMessageContext -Force # Force allows overwriting existing members. - } - else - { - $ScriptMessageContext = $ScriptMessage_Global_CachedServiceContext.$Service } + + # Update cached context data for the specified service. + $ScriptMessage_Global_CachedServiceContext | Add-Member -MemberType NoteProperty -Name $Service -Value $ScriptMessageContext -Force # Force allows overwriting existing members. } - - end + else { - return $ScriptMessageContext + $ScriptMessageContext = $ScriptMessage_Global_CachedServiceContext.$Service } + + return $ScriptMessageContext } \ No newline at end of file diff --git a/ScriptMessage/Public/Send-ScriptMessage.ps1 b/ScriptMessage/Public/Send-ScriptMessage.ps1 index d1fcdfb..2d39bcc 100644 --- a/ScriptMessage/Public/Send-ScriptMessage.ps1 +++ b/ScriptMessage/Public/Send-ScriptMessage.ps1 @@ -49,6 +49,12 @@ function Send-ScriptMessage .PARAMETER SenderId Specify the account used to send the message request. This might be different than the 'From' parameter in the case of "Send As', "Send on Behalf", delegated mailboxes, etc. If not specified, defaults to the address inside of the 'From' parameter. + .PARAMETER MailType # TODO: Implement OneOnOne emailing. + Override the default 'MailType' specified in the configuration file for the messaging service being used. Options are 'OneOnOne' or 'Group'. + .PARAMETER ChatType + Override the default 'ChatType' specified in the configuration file for the messaging service being used. Options are 'OneOnOne' or 'Group'. + .PARAMETER IncludeBCCInGroupChat + Specify whether to include BCC recipients in a group chat message. .EXAMPLE $MessageArguments = @{ @@ -221,6 +227,13 @@ function Send-ScriptMessage ValueFromPipelineByPropertyName = $true)] [string]$SenderId, + # TODO: Implement MailType (similar to ChatType so we can do 1:1 emailing) + [Parameter( + Mandatory = $false, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true)] + [MailType]$MailType, + [Parameter( Mandatory = $false, ValueFromPipeline = $true, @@ -235,97 +248,90 @@ function Send-ScriptMessage [Object]$IncludeBCCInGroupChat # Is an object so it can be set to $null ) - begin + # Set the necessary configuration variables. + $ScriptMessageConfig = Get-ScriptMessageConfig + + # Make sure that at least one of, To, CC, or BCC is provided. + if ([string]::IsNullOrEmpty($To) -and [string]::IsNullOrEmpty($CC) -and [string]::IsNullOrEmpty($BCC)) { - # Set the necessary configuration variables. - $ScriptMessageConfig = Get-ScriptMessageConfig + throw 'Please provide at least one parameter value for any of the following: To, CC, or BCC' } - process - { - # Make sure that at least one of, To, CC, or BCC is provided. - if ([string]::IsNullOrEmpty($To) -and [string]::IsNullOrEmpty($CC) -and [string]::IsNullOrEmpty($BCC)) - { - throw 'Please provide at least one parameter value for any of the following: To, CC, or BCC' - } + # Convert recipient types into properly formatted PSObject. + $From = ConvertTo-ScriptMessageRecipientObject -Recipient $From + Write-Host $($From.GetType()) # Note that From is NOT an array. There should only be one. + [array]$ReplyTo = ConvertTo-ScriptMessageRecipientObject -Recipient $ReplyTo + [array]$To = ConvertTo-ScriptMessageRecipientObject -Recipient $To + [array]$CC = ConvertTo-ScriptMessageRecipientObject -Recipient $CC + [array]$BCC = ConvertTo-ScriptMessageRecipientObject -Recipient $BCC - # Convert recipient types into properly formatted PSObject. - $From = ConvertTo-ScriptMessageRecipientObject -Recipient $From # Note that From is NOT an array. There should only be one. - [array]$ReplyTo = ConvertTo-ScriptMessageRecipientObject -Recipient $ReplyTo - [array]$To = ConvertTo-ScriptMessageRecipientObject -Recipient $To - [array]$CC = ConvertTo-ScriptMessageRecipientObject -Recipient $CC - [array]$BCC = ConvertTo-ScriptMessageRecipientObject -Recipient $BCC + # Convert body into properly formatted PSObject. + $Body = ConvertTo-ScriptMessageBodyObject -Body $Body - # Convert body into properly formatted PSObject. - $Body = ConvertTo-ScriptMessageBodyObject -Body $Body + # Convert attachments into properly formatted PSObject. + $Attachment = ConvertTo-ScriptMessageAttachmentObject -Attachment $Attachment + + if ($null -ne $Service) # If ServiceAndTypeSeparate + { + # Remove message service & message type duplicates. + $Service = $Service | Select-Object -Unique + $Type = $Type | Select-Object -Unique + + # Create the ServiceType class object\hash. + [MessageServiceType]$ServiceType = @{ + Service = $Service + Type = $Type + } + } - # Convert attachments into properly formatted PSObject. - $Attachment = ConvertTo-ScriptMessageAttachmentObject -Attachment $Attachment + foreach ($serviceTypeObj in $ServiceType) + { + # Set the connection parameters. + $ConnectionParameters = @{ + ServiceConfig = $ScriptMessageConfig.$($serviceTypeObj.Service) + } - if ($null -ne $Service) # If ServiceAndTypeSeparate + # Set default values if not specified by a parameter. + if (-not $ChatType) { - # Remove message service & message type duplicates. - $Service = $Service | Select-Object -Unique - $Type = $Type | Select-Object -Unique - - # Create the ServiceType class object\hash. - [MessageServiceType]$ServiceType = @{ - Service = $Service - Type = $Type - } + [ChatType]$ChatType = $ConnectionParameters.ServiceConfig.ChatType + } + if (-not $IncludeBCCInGroupChat) + { + [bool]$IncludeBCCInGroupChat = $ConnectionParameters.ServiceConfig.IncludeBCCInGroupChat } - foreach ($serviceTypeObj in $ServiceType) + # Connect to the messaging service, if necessary (e.g., API service). + Connect-ScriptMessage -Service $($serviceTypeObj.Service) -ErrorAction Stop + + switch ($($serviceTypeObj.Service)) { - # Set the connection parameters. - $ConnectionParameters = @{ - ServiceConfig = $ScriptMessageConfig.$($serviceTypeObj.Service) - } + 'MgGraph' { + $SendMessageParameters = [ordered]@{ + From = $From + ReplyTo = $ReplyTo + To = $To + CC = $CC + BCC = $BCC + SaveToSentItems = $SaveToSentItems + Subject = $Subject + Body = $Body + Attachment = $Attachment + SenderId = $SenderId + Type = $serviceTypeObj.Type + ChatType = $ChatType + IncludeBCCInGroupChat = $IncludeBCCInGroupChat + } - # Set default values if not specified by a parameter. - if (-not $ChatType) - { - [ChatType]$ChatType = $ConnectionParameters.ServiceConfig.ChatType - } - if (-not $IncludeBCCInGroupChat) - { - [bool]$IncludeBCCInGroupChat = $ConnectionParameters.ServiceConfig.IncludeBCCInGroupChat - } - - # Connect to the messaging service, if necessary (e.g., API service). - Connect-ScriptMessage -Service $($serviceTypeObj.Service) -ErrorAction Stop - - switch ($($serviceTypeObj.Service)) - { - 'MgGraph' { - $SendMessageParameters = [ordered]@{ - From = $From - ReplyTo = $ReplyTo - To = $To - CC = $CC - BCC = $BCC - SaveToSentItems = $SaveToSentItems - Subject = $Subject - Body = $Body - Attachment = $Attachment - SenderId = $SenderId - Type = $serviceTypeObj.Type - ChatType = $ChatType - IncludeBCCInGroupChat = $IncludeBCCInGroupChat - } - - Send-ScriptMessage_MgGraph @SendMessageParameters - - # Disconnect from Microsoft Graph API, if enabled in config. - if ($ConnectionParameters.ServiceConfig.MgDisconnectWhenDone) - { - $null = Disconnect-MgGraph -ErrorAction SilentlyContinue - } + Send-ScriptMessage_MgGraph @SendMessageParameters + + # Disconnect from Microsoft Graph API, if enabled in config. + if ($ConnectionParameters.ServiceConfig.MgDisconnectWhenDone) + { + $null = Disconnect-MgGraph -ErrorAction SilentlyContinue } - Default {throw "Invalid `'Service`' value."} } + Default {throw "Invalid `'Service`' value."} } } - - end {} } \ No newline at end of file diff --git a/ScriptMessage/Public/Set-ScriptMessageConfigFilePath.ps1 b/ScriptMessage/Public/Set-ScriptMessageConfigFilePath.ps1 index a802698..0333c40 100644 --- a/ScriptMessage/Public/Set-ScriptMessageConfigFilePath.ps1 +++ b/ScriptMessage/Public/Set-ScriptMessageConfigFilePath.ps1 @@ -28,12 +28,5 @@ function Set-ScriptMessageConfigFilePath [string]$Path ) - begin {} - - process - { - New-Variable -Name 'ScriptMessage_Global_ConfigFilePath' -Value $Path -Scope Global -Force - } - - end {} + New-Variable -Name 'ScriptMessage_Global_ConfigFilePath' -Value $Path -Scope Global -Force } \ No newline at end of file diff --git a/ScriptMessage/ScriptMessage.psm1 b/ScriptMessage/ScriptMessage.psm1 index da0025a..bc40bcd 100644 --- a/ScriptMessage/ScriptMessage.psm1 +++ b/ScriptMessage/ScriptMessage.psm1 @@ -18,6 +18,13 @@ enum MessageType { Chat } +# Public Enum +# Name: MailType +enum MailType { + OneOnOne + Group +} + # Public Enum # Name: ChatType enum ChatType { diff --git a/ScriptMessage/Services/MgGraph.ps1 b/ScriptMessage/Services/MgGraph.ps1 index d8d27dd..b7698f0 100644 --- a/ScriptMessage/Services/MgGraph.ps1 +++ b/ScriptMessage/Services/MgGraph.ps1 @@ -17,55 +17,46 @@ Function ConvertTo-IMicrosoftGraphRecipient [string]$Name ) - begin + # Return Null If Provided Recipient is Empty + if (([string]::IsNullOrEmpty($EmailAddress)) -and ([string]::IsNullOrEmpty($EmailAddress.AddressObj))) { - # Return Null If Provided Recipient is Empty - if (([string]::IsNullOrEmpty($EmailAddress)) -and ([string]::IsNullOrEmpty($EmailAddress.Address))) - { - return $null - } + return $null } - process + # Loop through each of the recipient parameter array objects + $IMicrosoftGraphRecipient = foreach ($address in $EmailAddress) { - # 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')) { - # 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)) { - # 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 + throw "Improperly formatted from, recipient, or reply to address." } - if ([string]::IsNullOrEmpty($Name)) - { - @{ - EmailAddress = @{Address = $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} - } + } + else + { + @{ + EmailAddress = [ordered]@{ + Name = $Name + Address = $address} } } } - end - { - return $IMicrosoftGraphRecipient - } + return $IMicrosoftGraphRecipient } function ConvertTo-IMicrosoftGraphItemBody @@ -86,19 +77,12 @@ function ConvertTo-IMicrosoftGraphItemBody [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 + $IMicrosoftGraphItemBody = + @{ + ContentType = $ContentType + Content = $Content } - - end {} + return $IMicrosoftGraphItemBody } Function ConvertTo-IMicrosoftGraphAttachment @@ -113,39 +97,30 @@ Function ConvertTo-IMicrosoftGraphAttachment [array]$Attachment ) - begin + if ([string]::IsNullOrEmpty($Attachment)) { - if ([string]::IsNullOrEmpty($Attachment)) - { - return $null - } + 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`'" + [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 - } + return $IMicrosoftGraphAttachment } Function ConvertTo-IMicrosoftGraphChatMessageAttachment @@ -160,38 +135,29 @@ Function ConvertTo-IMicrosoftGraphChatMessageAttachment [array]$MgDriveItem ) - begin + if ([string]::IsNullOrEmpty($MgDriveItem)) { - if ([string]::IsNullOrEmpty($MgDriveItem)) - { - return $null - } + 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`'" + + [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 - } + return $IMicrosoftGraphChatMessageAttachment } function ConvertTo-IMicrosoftGraphConversationMember @@ -207,41 +173,32 @@ function ConvertTo-IMicrosoftGraphConversationMember [pscustomobject]$EmailAddress ) - begin + # Return Null If Provided Recipient is Empty + if ([string]::IsNullOrEmpty($EmailAddress)) { - # Return Null If Provided Recipient is Empty - if ([string]::IsNullOrEmpty($EmailAddress)) - { - return $null - } + return $null } - process + # Loop through each of the recipient parameter array objects + $IMicrosoftGraphRecipient = foreach ($address in $EmailAddress) { - # 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')) { - # 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')" - } + throw "Improperly formatted from or recipient address." } - } - end - { - return $IMicrosoftGraphRecipient + # Return IMicrosoftGraphConversationMember + @{ + '@odata.type' = "#microsoft.graph.aadUserConversationMember" + roles = @( + "owner" + ) + "user@odata.bind" = "https://graph.microsoft.com/v1.0/users('$address')" + } } + + return $IMicrosoftGraphRecipient } function ConvertTo-IMicrosoftGraphDriveInvite @@ -257,45 +214,37 @@ function ConvertTo-IMicrosoftGraphDriveInvite [pscustomobject]$EmailAddress ) - begin + # Return Null If Provided Recipient is Empty + if ([string]::IsNullOrEmpty($EmailAddress)) { - # Return Null If Provided Recipient is Empty - if ([string]::IsNullOrEmpty($EmailAddress)) - { - return $null - } + return $null } - process + + # Loop through each of the recipient parameter array objects + [array]$IMicrosoftGraphDriveRecipient = foreach ($address in $EmailAddress) { - # 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')) { - # 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 - } + throw "Improperly formatted recipient address." } - $IMicrosoftGraphDriveInvite = @{ - recipients = $IMicrosoftGraphDriveRecipient - requireSignIn = $true - sendInvitation = $false - roles = @( - "read" - ) + # Return IMicrosoftGraphDriveRecipient + @{ + email = $address } } - end - { - return $IMicrosoftGraphDriveInvite + $IMicrosoftGraphDriveInvite = @{ + recipients = $IMicrosoftGraphDriveRecipient + requireSignIn = $true + sendInvitation = $false + roles = @( + "read" + ) } + + return $IMicrosoftGraphDriveInvite } function Connect-ScriptMessage_MgGraph @@ -309,158 +258,149 @@ function Connect-ScriptMessage_MgGraph [pscustomobject]$ServiceConfig ) - begin + # 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 - 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) + # 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')) { - 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 - } + # 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 + } + else + { + # Check for modules. + if (!(Get-Module -Name 'Microsoft.Graph.Users.Actions') -or !(Get-Module -Name 'Microsoft.Graph.Teams')) { - # 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 - } + # 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 + # 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) + 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.AllowableMessageTypes -contains 'Mail') + + if ($ServiceConfig.MgDelegatedPermission_RequestChatReadPermission -eq $true) { $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 + 'Chat.Read' # Allows an app to read 1 on 1 or group chats threads, on behalf of the signed-in user. ) } - if ($ServiceConfig.AllowableMessageTypes -contains 'Chat') + 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 += @( - '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. + '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" - if ($ServiceConfig.MgDelegatedPermission_RequestChatReadPermission -eq $true) + 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') { - $MicrosoftGraphScopes += @( - 'Chat.Read' # Allows an app to read 1 on 1 or group chats threads, on behalf of the signed-in user. - ) + $NewMessage = "Connecting to Microsoft Graph using a certificate file is only supported with PowerShell version 7.4 and later." + throw $NewMessage } - 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) + + $MgApp_CertificatePath = $ExecutionContext.InvokeCommand.ExpandString($ServiceConfig.MgApp_CertificatePath) + + # Try accessing private key certificate without password using current process credentials. + [X509Certificate]$MgApp_Certificate = $null + try { - $MicrosoftGraphScopes += @( - 'Files.ReadWrite' # Allows the app to read, create, update and delete the signed-in user's files. - ) + [X509Certificate]$MgApp_Certificate = Get-PfxCertificate -FilePath $MgApp_CertificatePath -NoPromptForPassword } - } - $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') + catch # If that doesn't work try the included credentials. + { + $MgApp_EncryptedCertificatePassword = $ServiceConfig.MgApp_EncryptedCertificatePassword + if ([string]::IsNullOrEmpty($MgApp_EncryptedCertificatePassword)) { - $NewMessage = "Connecting to Microsoft Graph using a certificate file is only supported with PowerShell version 7.4 and later." + $NewMessage = "Cannot access .pfx private key certificate file and no password has been provided." 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. + else { - $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 - } + [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."} + + $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."} } + Default {throw "Invalid `'MgPermissionType`' value."} } - - end {} } function Disconnect-ScriptMessage_MGGraph @@ -554,138 +494,133 @@ function Send-ScriptMessage_MgGraph [bool]$IncludeBCCInGroupChat ) - begin - { - # Set the Service ID. - $ServiceId = 'MgGraph' - } + # Set the Service ID. + $ServiceId = 'MgGraph' - process + # Send the message on each supported service specified. + foreach ($typeItem in $Type) { - # Send the message on each supported service specified. - foreach ($typeItem in $Type) - { - # Reset Warnings - $MgWarningMessages = @() + # 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)) + 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' { - 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 - } + [hashtable]$Message['Body'] = ConvertTo-IMicrosoftGraphItemBody -Content $Body.Content } - - # Check For Separate 'SenderID' Value. Make equal to 'From' if not provided. - if ([string]::IsNullOrEmpty($SenderId)) + else { - $SenderId = $Message.From.emailAddress.Address + [hashtable]$Message['Body'] = ConvertTo-IMicrosoftGraphItemBody -Content $Body.Content -ContentType $Body.ContentType } - - # Send Email. - $SendEmailMessageResult = Send-MgUserMail -UserId $SenderId -BodyParameter $EmailParams -PassThru - - # Collect Return Info - $SendScriptMessageResult_SentFrom = [PSCustomObject]@{ - Name = $From.Name - Address = $From.AddressObj + } + [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 } - [array]$SendScriptMessageResult_Recipients_To = foreach ($i in $To) - { - [PSCustomObject]@{ - Name = $i.Name - Address = $i.AddressObj - } + } + + # Check For Separate 'SenderID' Value. Make equal to 'From' if not provided. + if ([string]::IsNullOrEmpty($SenderId)) + { + $SenderId = $Message.From.emailAddress.Address # Note: This is correct as 'xxxx.Address' (not 'AddressObj'). It is converted Microsoft's to IMicrosoftGraphRecipient. + } + + # 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_CC = foreach ($i in $CC) + { + [PSCustomObject]@{ + Name = $i.Name + Address = $i.AddressObj } - [array]$SendScriptMessageResult_Recipients_BCC = foreach ($i in $BCC) + } + [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 doesn't have a method called Address) so we don't get the dreaded 'OverloadDefinitions'. + if ($null -ne $SendScriptMessageResult_Recipients_To) { - [PSCustomObject]@{ - Name = $i.Name - Address = $i.AddressObj - } + [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_To).Address } - [array]$SendScriptMessageResult_Recipients_All = @( # Since Address is also a PSMethod we need to do some fun stuff (List doesn't have a method called Address) so we don't get the dreaded 'OverloadDefinitions'. - if ($null -ne $SendScriptMessageResult_Recipients_To) - { - [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_To).Address - } - if ($null -ne $SendScriptMessageResult_Recipients_CC) - { - [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_CC).Address - } - if ($null -ne $SendScriptMessageResult_Recipients_BCC) - { - [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 + if ($null -ne $SendScriptMessageResult_Recipients_CC) + { + [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_CC).Address } - - $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 ($null -ne $SendScriptMessageResult_Recipients_BCC) + { + [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 + } - # If successful, output result info. - $SendScriptMessageResult + $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 } - 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 = "Chat not sent. 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 #TODO: Remove this continue so we can get the warning message in the output object. See what we may have to change below (probably skip the section). - } + # 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 = "Chat not sent. 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" + } + else + { # Grab the latest MgGraph service context. $MgGraphContext = Get-ScriptMessageContext -Service $ServiceId @@ -701,208 +636,209 @@ function Send-ScriptMessage_MgGraph $NewMessage = "Chat not sent. Microsoft Graph does not support sending Chat messages on behalf of others." Write-Warning -Message $NewMessage $MgWarningMessages += "$NewMessage" - continue #TODO: Remove this continue so we can get the warning message in the output object. See what we may have to change below (probably skip the section). - } - - # 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. - # (Graph does not support sending direct chat messages to yourself since that's not a standard chat thread. I think it's some sort of "note" when used by Teams.) - [array]$ChatRecipients = $ChatRecipients | Sort-Object -Unique | Where-Object {$_ -ne $SenderId} + # 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}) - # Collect all chat participants. - [array]$AllChatParticipants = [array]$SenderId + [array]$ChatRecipients - - # Process chat only there are recipients. Otherwise warn if no chat recipients - if ($ChatRecipients.Count -eq 0) - { - $NewMessage = "Chat not sent. No chat recipients exist. If you are trying to send a chat message to yourself, please note that Microsoft doesn't support direct messaging to yourself via the Graph API." - Write-Warning -Message $NewMessage - $MgWarningMessages += "$NewMessage" - } - else - { - # 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) + [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. + # (Graph does not support sending direct chat messages to yourself since that's not a standard chat thread. I think it's some sort of "note" when used by Teams.) + [array]$ChatRecipients = $ChatRecipients | Sort-Object -Unique | Where-Object {$_ -ne $SenderId} + + # Collect all chat participants. + [array]$AllChatParticipants = [array]$SenderId + [array]$ChatRecipients + + # Process chat only there are recipients. Otherwise warn if no chat recipients + if ($ChatRecipients.Count -eq 0) + { + $NewMessage = "Chat not sent. No chat recipients exist. If you are trying to send a chat message to yourself, please note that Microsoft doesn't support direct messaging to yourself via the Graph API." + Write-Warning -Message $NewMessage + $MgWarningMessages += "$NewMessage" + } + else + { + # 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)) { - if ($chatRecipient_BCC -notin $AllChatParticipants) + foreach ($chatRecipient_BCC in $ChatRecipients_BCC) { - $NewMessage = "The following BCC recipient is not included in the group chat: $chatRecipient_BCC" - Write-Warning -Message $NewMessage - $MgWarningMessages += "$NewMessage" + 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) + # 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)) { - $MgGraphDriveEndpointUri = 'https://graph.microsoft.com/v1.0/drives/' - $AttachmentFileName = $attachmentItem.Name + # 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 + } - # 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 + # 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 } - # 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) + # Convert Parameters to IMicrosoft* + $Message = @{} + if (-not [string]::IsNullOrEmpty($Body.Content)) { - $DriveInviteResult = Invoke-MgInviteDriveItem -DriveId $MgUserDrive.Id -DriveItemId $UploadDriveItemResult.id -BodyParameter $DriveInviteParams + 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) - # 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 + $ChatParams = [ordered]@{ + Body = $Message.Body + Attachments = $Message.Attachment + } } 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 + $ChatParams = [ordered]@{ + Body = $Message.Body + } } - } - # Create a new chat object, if needed, & send the message. - $Member_SenderID = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $SenderId) + # Create a new chat object, if needed, & send the message. + $Member_SenderID = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $SenderId) - switch ($ChatType) - { - OneOnOne + switch ($ChatType) { - foreach ($chatRecipient in $ChatRecipients) + 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. { - $Member_ChatRecipients = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $chatRecipient) + # Collect Group Members + [array]$Member_ChatRecipients = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $ChatRecipients) [array]$Message['Members'] = [array]$Member_SenderID + [array]$Member_ChatRecipients - try + + # 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') { - $NewChatResult = New-MgChat -ChatType $ChatType.ToString() -Members $Message.Members - $SendChatMessageResult = New-MgChatMessage -ChatId $NewChatResult.Id -BodyParameter $ChatParams + # 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 } - catch + else # Only has Chat.ReadBasic so we can't see the last message preview. { - $NewMessage = "Cannot create a chat with the recipient '$($chatRecipient)'." - Write-Warning -Message $NewMessage - $MgWarningMessages += "$NewMessage" + # 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 } - } - } - 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)) + + # Reset the variable and then do a compare\search + $LatestExistingGroupChatMatch = $null + foreach ($existingGroupChat in $ExistingGroupChats) { - $LatestExistingGroupChatMatch = $existingGroupChat + 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 + # Send the chat message; create a new chat group if needed. + if (-not $LatestExistingGroupChatMatch) { - $NewChatResult = New-MgChat -ChatType $ChatType.ToString() -Members $Message.Members - $ChatToUse = $NewChatResult - $SendChatMessageResult = New-MgChatMessage -ChatId $ChatToUse.Id -BodyParameter $ChatParams + 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" + } } - catch + else { - $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" + $ChatToUse = $LatestExistingGroupChatMatch + $SendChatMessageResult = New-MgChatMessage -ChatId $ChatToUse.Id -BodyParameter $ChatParams } } - else - { - $ChatToUse = $LatestExistingGroupChatMatch - $SendChatMessageResult = New-MgChatMessage -ChatId $ChatToUse.Id -BodyParameter $ChatParams - } } } } @@ -955,43 +891,41 @@ function Send-ScriptMessage_MgGraph BCC = $SendScriptMessageResult_Recipients_BCC All = $SendScriptMessageResult_Recipients_All IncludeBCCInGroupChat = $SendScriptMessageResult_Recipients_IncludeBCCInGroupChat - } + } + } - # Compile Caught Errors and Warnings - if ($MgWarningMessages.Count -gt 0) + # Compile Caught Errors and Warnings + if ($MgWarningMessages.Count -gt 0) + { + [array]$SendScriptMessageResult_Error = foreach ($mgWarningMessage in $MgWarningMessages) { - [array]$SendScriptMessageResult_Error = foreach ($mgWarningMessage in $MgWarningMessages) - { - [PSCustomObject]@{ - Type = 'Warning' - Message = $mgWarningMessage - } + [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)'." + 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 {} } \ No newline at end of file From 179a6fcc27855560c7d622f97d5b92a16b246a15 Mon Sep 17 00:00:00 2001 From: Sekers <46898253+Sekers@users.noreply.github.com> Date: Wed, 15 Oct 2025 16:45:25 -0500 Subject: [PATCH 03/15] CATCH IF TEAMS CHAT ATTACHMENTS FOLDER DOES NOT ALREADY EXIST --- ScriptMessage/Services/MgGraph.ps1 | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/ScriptMessage/Services/MgGraph.ps1 b/ScriptMessage/Services/MgGraph.ps1 index b7698f0..fbb88a2 100644 --- a/ScriptMessage/Services/MgGraph.ps1 +++ b/ScriptMessage/Services/MgGraph.ps1 @@ -702,7 +702,23 @@ function Send-ScriptMessage_MgGraph $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 + try # Need to check if the $TeamsChatFolder exists first. If not, Get-MgDriveItem will throw a terminating exception. + { + $ExistingFiles = (Get-MgDriveItem -DriveId $MgUserDrive.Id -DriveItemId $TeamsChatFolder -ExpandProperty 'Children').Children + } + catch + { + if ($_.Exception.Message -like "*itemNotFound*") + { + $ExistingFiles = $null # Make sure this is null. + } + else + { + # Handle other types of errors + Write-Error "An unexpected error occurred while uploading file attachment for chat: $($_.Exception.Message)" + } + } + $FileNameCounter = 0 while ($ExistingFiles.Name -contains $AttachmentFileName) { From 164fbf13e8e2b7ca18c8167a37c8ab1dca1922e4 Mon Sep 17 00:00:00 2001 From: Sekers <46898253+Sekers@users.noreply.github.com> Date: Wed, 15 Oct 2025 16:50:49 -0500 Subject: [PATCH 04/15] REMOVE LEFTOVER DEBUG LINE --- ScriptMessage/Public/Send-ScriptMessage.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ScriptMessage/Public/Send-ScriptMessage.ps1 b/ScriptMessage/Public/Send-ScriptMessage.ps1 index 2d39bcc..0b0add6 100644 --- a/ScriptMessage/Public/Send-ScriptMessage.ps1 +++ b/ScriptMessage/Public/Send-ScriptMessage.ps1 @@ -258,8 +258,7 @@ function Send-ScriptMessage } # Convert recipient types into properly formatted PSObject. - $From = ConvertTo-ScriptMessageRecipientObject -Recipient $From - Write-Host $($From.GetType()) # Note that From is NOT an array. There should only be one. + $From = ConvertTo-ScriptMessageRecipientObject -Recipient $From # Note that From is NOT an array. There should only be one. [array]$ReplyTo = ConvertTo-ScriptMessageRecipientObject -Recipient $ReplyTo [array]$To = ConvertTo-ScriptMessageRecipientObject -Recipient $To [array]$CC = ConvertTo-ScriptMessageRecipientObject -Recipient $CC From bbcfa24c39e2c73fdb78f4c86d881c8ce2f4176a Mon Sep 17 00:00:00 2001 From: Sekers <46898253+Sekers@users.noreply.github.com> Date: Wed, 15 Oct 2025 17:03:09 -0500 Subject: [PATCH 05/15] SET UP PREP WORK FOR COLLECTING WARNINGS WITH EMAIL IN MGGRAPH --- ScriptMessage/Services/MgGraph.ps1 | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/ScriptMessage/Services/MgGraph.ps1 b/ScriptMessage/Services/MgGraph.ps1 index fbb88a2..5cb0988 100644 --- a/ScriptMessage/Services/MgGraph.ps1 +++ b/ScriptMessage/Services/MgGraph.ps1 @@ -598,11 +598,28 @@ function Send-ScriptMessage_MgGraph All = $SendScriptMessageResult_Recipients_All } + # 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 + MailType = $MailType # TODO: MAILTYPE Status = $SendEmailMessageResult # The SDK only returns $true and nothing else (and only that because of the 'PassThru') - Error = $null + Error = $SendScriptMessageResult_Error SentFrom = $SendScriptMessageResult_SentFrom Recipients = $SendScriptMessageResult_Recipients } @@ -859,7 +876,7 @@ function Send-ScriptMessage_MgGraph } } - # Collect Return Info # TODO: How does this work if we have both chat and email service types. I also think that the email type looks different (better) when CC and BBC are missing, etc. + # Collect Return Info $SendScriptMessageResult_SentFrom = [PSCustomObject]@{ Name = $From.Name Address = $From.AddressObj From 496cd32e07b2139decdbe238b57c7fdf0f4cbeae Mon Sep 17 00:00:00 2001 From: Sekers <46898253+Sekers@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:27:12 -0500 Subject: [PATCH 06/15] CHANGES TO ONLY IMPORT NECESSARY GRAPH MODULES --- .../Public/Get-ScriptMessageConfig.ps1 | 2 +- ScriptMessage/Services/MgGraph.ps1 | 58 ++++++++++++------- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/ScriptMessage/Public/Get-ScriptMessageConfig.ps1 b/ScriptMessage/Public/Get-ScriptMessageConfig.ps1 index 1cad342..7b02d66 100644 --- a/ScriptMessage/Public/Get-ScriptMessageConfig.ps1 +++ b/ScriptMessage/Public/Get-ScriptMessageConfig.ps1 @@ -8,7 +8,7 @@ function Get-ScriptMessageConfig Get the configuration and secrets to connect to the messaging service(s). .DESCRIPTION - Get the configuration and secrets to connect to the messaging service(s). + Get the configuration and secrets to connect to the messaging service(s). .PARAMETER ConfigPath Optional. If not provided, the function will use the path used in the current session (if set). diff --git a/ScriptMessage/Services/MgGraph.ps1 b/ScriptMessage/Services/MgGraph.ps1 index 5cb0988..cf92d32 100644 --- a/ScriptMessage/Services/MgGraph.ps1 +++ b/ScriptMessage/Services/MgGraph.ps1 @@ -258,33 +258,46 @@ function Connect-ScriptMessage_MgGraph [pscustomobject]$ServiceConfig ) - # Check For Microsoft.Graph Module #TODO: Don't check and import the modules unless needed, depending on allowed services. Chat & Mail + # Check For Microsoft.Graph Modules # 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) + $RequiredModules = [System.Collections.Generic.List[Object]]::new() + + # Required For All Graph Modules + [string]$ModuleName = 'Microsoft.Graph.Authentication' # Used for Connect-MgGraph, Disconnect-MgGraph, & Get-MgContext. A required module for all Graph modules. + Import-Module -Name $ModuleName -ErrorAction SilentlyContinue + $RequiredModules.Add($ModuleName) + + # If Mail is enabled (based on the config item 'AllowableMessageTypes' containing 'Mail'), check for the needed module. + if ($ServiceConfig.AllowableMessageTypes -contains 'Mail') { - Import-Module 'Microsoft.Graph.Files' -ErrorAction SilentlyContinue # Used for Get-MgUserDrive + [string]$ModuleName = 'Microsoft.Graph.Users.Actions' # Used for Send-MgUserMail. + Import-Module -Name $ModuleName -ErrorAction SilentlyContinue + $RequiredModules.Add($ModuleName) + } + + # If Chat is enabled (based on the config item 'AllowableMessageTypes' containing 'Chat'), check for the needed module. + if ($ServiceConfig.AllowableMessageTypes -contains 'Chat') + { + [string]$ModuleName = 'Microsoft.Graph.Teams' # Used for New-MgChat & New-MgChatMessage. + Import-Module -Name $ModuleName -ErrorAction SilentlyContinue + $RequiredModules.Add($ModuleName) + } - # 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 - } + # If 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) + { + [string]$ModuleName = 'Microsoft.Graph.Files' # Used for Get-MgUserDrive + Import-Module -Name $ModuleName -ErrorAction SilentlyContinue + $RequiredModules.Add($ModuleName) } - else + + # Check for missing Graph modules. + $ImportedModules = Get-Module + $MissingModules = Compare-Object -ReferenceObject $RequiredModules -DifferenceObject $ImportedModules | Where-Object { $_.SideIndicator -eq '<=' } | Select-Object -ExpandProperty InputObject + if ($MissingModules.Count -gt 0) { - # 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 - } + Write-Error "Please first install the following sub-modules from https://www.powershellgallery.com/packages/Microsoft.Graph/: $($MissingModules -join ', ')" + Return } # Connect to the Microsoft Graph API. @@ -526,6 +539,7 @@ function Send-ScriptMessage_MgGraph } [array]$Message['Attachment'] = ConvertTo-IMicrosoftGraphAttachment -Attachment $Attachment + # TODO: Allow OPTION For Files To Be Shared Via OneDrive/SharePoint Instead Of As Direct Attachments. Perhaps add a switch parameter to override the default config. Maybe even have a config option of use OneDrive when over x Bytes. Set to 0 to always. # Build Email $EmailParams = [ordered]@{ SaveToSentItems = $SaveToSentItems From 23a54aa6cb364371ad91157afc11f5aaca27987c Mon Sep 17 00:00:00 2001 From: Sekers <46898253+Sekers@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:21:06 -0500 Subject: [PATCH 07/15] Minor syntax error fix in built-in help --- ScriptMessage/Public/Send-ScriptMessage.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ScriptMessage/Public/Send-ScriptMessage.ps1 b/ScriptMessage/Public/Send-ScriptMessage.ps1 index 0b0add6..29b09fa 100644 --- a/ScriptMessage/Public/Send-ScriptMessage.ps1 +++ b/ScriptMessage/Public/Send-ScriptMessage.ps1 @@ -79,7 +79,7 @@ function Send-ScriptMessage } } - Send-ScriptMessage -Service -Type 'Mail', 'Chat' MgGraph @MessageArguments + Send-ScriptMessage -Service MgGraph -Type 'Mail', 'Chat' @MessageArguments .EXAMPLE $MessageArguments = @{ From = @{ From b1a4f661867a365c05d730868c1d1aad8aec7fb9 Mon Sep 17 00:00:00 2001 From: Sekers <46898253+Sekers@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:28:29 -0500 Subject: [PATCH 08/15] RENAME MgGraph Service to MicrosoftGraph --- .../Public/Connect-ScriptMessage.ps1 | 6 ++--- .../Public/Disconnect-ScriptMessage.ps1 | 8 +++---- .../Public/Get-ScriptMessageContext.ps1 | 6 ++--- ScriptMessage/Public/Send-ScriptMessage.ps1 | 10 ++++----- ScriptMessage/ScriptMessage.psm1 | 2 +- .../{MgGraph.ps1 => MicrosoftGraph.ps1} | 22 +++++++++---------- Templates/config_scriptmessage.json | 2 +- Tests/MgGraphDynamicParams.ps1 | 8 +++---- 8 files changed, 32 insertions(+), 32 deletions(-) rename ScriptMessage/Services/{MgGraph.ps1 => MicrosoftGraph.ps1} (98%) diff --git a/ScriptMessage/Public/Connect-ScriptMessage.ps1 b/ScriptMessage/Public/Connect-ScriptMessage.ps1 index b0aad2d..2e12d5f 100644 --- a/ScriptMessage/Public/Connect-ScriptMessage.ps1 +++ b/ScriptMessage/Public/Connect-ScriptMessage.ps1 @@ -16,9 +16,9 @@ function Connect-ScriptMessage Returns connection information after performing function. .EXAMPLE - Connect-ScriptMessage -Service MgGraph + Connect-ScriptMessage -Service MicrosoftGraph .EXAMPLE - Connect-ScriptMessage -Service MgGraph -ReturnConnectionInfo + Connect-ScriptMessage -Service MicrosoftGraph -ReturnConnectionInfo #> [CmdletBinding()] @@ -48,7 +48,7 @@ function Connect-ScriptMessage # Connect to the proper service. switch ($Service) { - MgGraph {Connect-ScriptMessage_MGGraph @ConnectionParameters} + MicrosoftGraph {Connect-ScriptMessage_MicrosoftGraph @ConnectionParameters} } # Return the connection information, if requested. diff --git a/ScriptMessage/Public/Disconnect-ScriptMessage.ps1 b/ScriptMessage/Public/Disconnect-ScriptMessage.ps1 index 195ce0e..4f29b3d 100644 --- a/ScriptMessage/Public/Disconnect-ScriptMessage.ps1 +++ b/ScriptMessage/Public/Disconnect-ScriptMessage.ps1 @@ -16,9 +16,9 @@ function Disconnect-ScriptMessage Returns connection information after performing function. .EXAMPLE - Disconnect-ScriptMessage -Service MgGraph + Disconnect-ScriptMessage -Service MicrosoftGraph .EXAMPLE - Disconnect-ScriptMessage -Service MgGraph -ReturnConnectionInfo + Disconnect-ScriptMessage -Service MicrosoftGraph -ReturnConnectionInfo #> [CmdletBinding()] @@ -40,7 +40,7 @@ function Disconnect-ScriptMessage # Disconnect from the proper service. $ServiceDisconnectReturnInfo = switch ($Service) { - MgGraph {Disconnect-ScriptMessage_MGGraph} + MicrosoftGraph {Disconnect-ScriptMessage_MicrosoftGraph} } # Return the disconnection information, if requested. @@ -61,7 +61,7 @@ function Disconnect-ScriptMessage # Add in disconnection information. switch ($Service) { - MgGraph { + MicrosoftGraph { if ([string]::IsNullOrEmpty($ServiceDisconnectReturnInfo)) { break # Terminate the switch statement. diff --git a/ScriptMessage/Public/Get-ScriptMessageContext.ps1 b/ScriptMessage/Public/Get-ScriptMessageContext.ps1 index 4076721..3e15b11 100644 --- a/ScriptMessage/Public/Get-ScriptMessageContext.ps1 +++ b/ScriptMessage/Public/Get-ScriptMessageContext.ps1 @@ -17,10 +17,10 @@ function Get-ScriptMessageContext Returns the cached context information if it exists to reduce API calls. .EXAMPLE - Get-ScriptMessageContext -Service MgGraph + Get-ScriptMessageContext -Service MicrosoftGraph .EXAMPLE - Get-ScriptMessageContext -Service MgGraph -ReturnCachedContext + Get-ScriptMessageContext -Service MicrosoftGraph -ReturnCachedContext #> [CmdletBinding()] @@ -61,7 +61,7 @@ function Get-ScriptMessageContext # Retrieve connection information. switch ($Service) { - MgGraph { + MicrosoftGraph { $MgContext = Get-MgContext if ([string]::IsNullOrEmpty($MgContext)) { diff --git a/ScriptMessage/Public/Send-ScriptMessage.ps1 b/ScriptMessage/Public/Send-ScriptMessage.ps1 index 29b09fa..8fd7b2f 100644 --- a/ScriptMessage/Public/Send-ScriptMessage.ps1 +++ b/ScriptMessage/Public/Send-ScriptMessage.ps1 @@ -67,7 +67,7 @@ function Send-ScriptMessage } } - Send-ScriptMessage -Service MgGraph -Type 'Mail' @MessageArguments + Send-ScriptMessage -Service MicrosoftGraph -Type 'Mail' @MessageArguments .EXAMPLE $MessageArguments = @{ From = 'jdoe@domain.com' @@ -79,7 +79,7 @@ function Send-ScriptMessage } } - Send-ScriptMessage -Service MgGraph -Type 'Mail', 'Chat' @MessageArguments + Send-ScriptMessage -Service MicrosoftGraph -Type 'Mail', 'Chat' @MessageArguments .EXAMPLE $MessageArguments = @{ From = @{ @@ -102,7 +102,7 @@ function Send-ScriptMessage SenderId = 'senderaccount@domain.com' } - Send-ScriptMessage -Service MgGraph @MessageArguments + Send-ScriptMessage -Service MicrosoftGraph @MessageArguments .EXAMPLE # Attachments From Variable - Option 1: PS Desktop or Core $Content1 = [System.IO.File]::ReadAllBytes('C:\Users\John\Downloads\MyPDF.pdf') @@ -305,7 +305,7 @@ function Send-ScriptMessage switch ($($serviceTypeObj.Service)) { - 'MgGraph' { + 'MicrosoftGraph' { $SendMessageParameters = [ordered]@{ From = $From ReplyTo = $ReplyTo @@ -322,7 +322,7 @@ function Send-ScriptMessage IncludeBCCInGroupChat = $IncludeBCCInGroupChat } - Send-ScriptMessage_MgGraph @SendMessageParameters + Send-ScriptMessage_MicrosoftGraph @SendMessageParameters # Disconnect from Microsoft Graph API, if enabled in config. if ($ConnectionParameters.ServiceConfig.MgDisconnectWhenDone) diff --git a/ScriptMessage/ScriptMessage.psm1 b/ScriptMessage/ScriptMessage.psm1 index bc40bcd..8daa981 100644 --- a/ScriptMessage/ScriptMessage.psm1 +++ b/ScriptMessage/ScriptMessage.psm1 @@ -8,7 +8,7 @@ New-Variable -Name 'ScriptMessage_Global_CachedServiceContext' -Value ([PSCustom # Public Enum # Name: MessagingService enum MessagingService { - MgGraph + MicrosoftGraph } # Public Enum diff --git a/ScriptMessage/Services/MgGraph.ps1 b/ScriptMessage/Services/MicrosoftGraph.ps1 similarity index 98% rename from ScriptMessage/Services/MgGraph.ps1 rename to ScriptMessage/Services/MicrosoftGraph.ps1 index cf92d32..b7aa72b 100644 --- a/ScriptMessage/Services/MgGraph.ps1 +++ b/ScriptMessage/Services/MicrosoftGraph.ps1 @@ -247,7 +247,7 @@ function ConvertTo-IMicrosoftGraphDriveInvite return $IMicrosoftGraphDriveInvite } -function Connect-ScriptMessage_MgGraph +function Connect-ScriptMessage_MicrosoftGraph { [CmdletBinding()] param( @@ -258,7 +258,7 @@ function Connect-ScriptMessage_MgGraph [pscustomobject]$ServiceConfig ) - # Check For Microsoft.Graph Modules + # Check For MicrosoftGraph Modules # Don't import the entire 'Microsoft.Graph' module. Only import the needed sub-modules. $RequiredModules = [System.Collections.Generic.List[Object]]::new() @@ -416,12 +416,12 @@ function Connect-ScriptMessage_MgGraph } } -function Disconnect-ScriptMessage_MGGraph +function Disconnect-ScriptMessage_MicrosoftGraph { return Disconnect-MgGraph } -function Send-ScriptMessage_MgGraph +function Send-ScriptMessage_MicrosoftGraph { [CmdletBinding()] param( @@ -508,7 +508,7 @@ function Send-ScriptMessage_MgGraph ) # Set the Service ID. - $ServiceId = 'MgGraph' + $ServiceId = 'MicrosoftGraph' # Send the message on each supported service specified. foreach ($typeItem in $Type) @@ -652,8 +652,8 @@ function Send-ScriptMessage_MgGraph } else { - # Grab the latest MgGraph service context. - $MgGraphContext = Get-ScriptMessageContext -Service $ServiceId + # Grab the latest MicrosoftGraph service context. + $MicrosoftGraphContext = Get-ScriptMessageContext -Service $ServiceId # Check For Separate 'SenderID' Value. Make equal to 'From' if not provided. if ([string]::IsNullOrEmpty($SenderId)) @@ -724,12 +724,12 @@ function Send-ScriptMessage_MgGraph if (-not [string]::IsNullOrEmpty($Attachment)) { # Upload the attached file(s) to OneDrive. - $MgUserDrive = Get-MgUserDrive -UserId $($MgGraphContext.Account) + $MgUserDrive = Get-MgUserDrive -UserId $($MicrosoftGraphContext.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/' + $MicrosoftGraphDriveEndpointUri = '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. @@ -761,7 +761,7 @@ function Send-ScriptMessage_MgGraph # Upload File # TODO: Test weird characters in filename like pound or something $DriveItemId = "$TeamsChatFolder/$($AttachmentFileName):" - $InvokeUri = $($MgGraphDriveEndpointUri + $MgUserDrive.Id + '/' + $DriveItemId + '/content') + $InvokeUri = $($MicrosoftGraphDriveEndpointUri + $MgUserDrive.Id + '/' + $DriveItemId + '/content') # Output the drive upload result. #Set-MgDriveItemContent -DriveId $MgUserDrive.Id -DriveItemId $DriveItemId -InFile $Attachment[0] # Overwrites file if it exists @@ -840,7 +840,7 @@ function Send-ScriptMessage_MgGraph ) # 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 + [array]$MicrosoftGraphScopes = $MicrosoftGraphContext | 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. diff --git a/Templates/config_scriptmessage.json b/Templates/config_scriptmessage.json index a7ea11c..2344afc 100644 --- a/Templates/config_scriptmessage.json +++ b/Templates/config_scriptmessage.json @@ -1,5 +1,5 @@ { - "MgGraph": { + "MicrosoftGraph": { "AllowableMessageTypes": ["Mail", "Chat"], "ChatType": "Group", "IncludeBCCInGroupChat": false, diff --git a/Tests/MgGraphDynamicParams.ps1 b/Tests/MgGraphDynamicParams.ps1 index 66e74a9..2b634ad 100644 --- a/Tests/MgGraphDynamicParams.ps1 +++ b/Tests/MgGraphDynamicParams.ps1 @@ -3,11 +3,11 @@ DynamicParam # Initialize Parameter Dictionary $ParameterDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new() - # Make Mg* parameters appear only if messaging 'Service' is 'MgGraph'. - if ($Service -eq 'MgGraph') + # Make Mg* parameters appear only if messaging 'Service' is 'MicrosoftGraph'. + if ($Service -eq 'MicrosoftGraph') { $ParameterAttributes = [System.Management.Automation.ParameterAttribute]@{ - ParameterSetName = "MgGraph" + ParameterSetName = "MicrosoftGraph" Mandatory = $true ValueFromPipeline = $true ValueFromPipelineByPropertyName = $true @@ -42,7 +42,7 @@ begin # Set Variables From Dynamic Parameters switch ($Service) { - 'MgGraph' { + 'MicrosoftGraph' { $MgPermissionType = $PSBoundParameters['MgPermissionType'] $MgDisconnectWhenDone = $PSBoundParameters['MgDisconnectWhenDone'] $MgTenantID = $PSBoundParameters['MgTenantID'] From 494d42f323e2fd8097ab2c110c91e8c6565d490a Mon Sep 17 00:00:00 2001 From: Sekers <46898253+Sekers@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:46:20 -0500 Subject: [PATCH 09/15] Get-ScriptMessageConfig CAN NOW PULL DATA FOR SPECIFIC SERVICES INSTEAD OF EVERYTHING. --- .../Public/Get-ScriptMessageConfig.ps1 | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/ScriptMessage/Public/Get-ScriptMessageConfig.ps1 b/ScriptMessage/Public/Get-ScriptMessageConfig.ps1 index 7b02d66..4545918 100644 --- a/ScriptMessage/Public/Get-ScriptMessageConfig.ps1 +++ b/ScriptMessage/Public/Get-ScriptMessageConfig.ps1 @@ -12,11 +12,15 @@ function Get-ScriptMessageConfig .PARAMETER ConfigPath Optional. If not provided, the function will use the path used in the current session (if set). + .PARAMETER Service + Optional. Return only the info related to a specific service. .EXAMPLE Get-ScriptMessageConfig .EXAMPLE Get-ScriptMessageConfig -Path '.\Config\config_scriptmessage.json' + .EXAMPLE + Get-ScriptMessageConfig -Service MicrosoftGraph #> [CmdletBinding()] @@ -25,7 +29,14 @@ function Get-ScriptMessageConfig Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] - [string]$Path = $ScriptMessage_Global_ConfigFilePath # If not entered will see if it can pull path from this variable. + [string]$Path = $ScriptMessage_Global_ConfigFilePath, # If not entered will see if it can pull path from this variable. + + [Parameter( + Position=1, + Mandatory = $false, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true)] + [MessagingService]$Service ) # Make Sure Requested Path Isn't Null or Empty (better to catch it here than validating on the parameter of this function) @@ -38,7 +49,14 @@ function Get-ScriptMessageConfig try { $ScriptMessageConfig = Get-Content -Path "$Path" -ErrorAction 'Stop' | ConvertFrom-Json - return $ScriptMessageConfig + if (-not ($null -eq $Service)) + { + return $ScriptMessageConfig.$Service + } + else + { + return $ScriptMessageConfig + } } catch { From a5fb90cdd7c802bb70acee3e30608f30f6a8aa65 Mon Sep 17 00:00:00 2001 From: Sekers <46898253+Sekers@users.noreply.github.com> Date: Thu, 16 Oct 2025 15:49:17 -0500 Subject: [PATCH 10/15] FIXED OVERLOOKING AllowableMessageTypes SETTING FOR MS GRAPH SERVICE. --- .../Public/Connect-ScriptMessage.ps1 | 5 +- ScriptMessage/Services/MicrosoftGraph.ps1 | 562 +++++++++--------- 2 files changed, 298 insertions(+), 269 deletions(-) diff --git a/ScriptMessage/Public/Connect-ScriptMessage.ps1 b/ScriptMessage/Public/Connect-ScriptMessage.ps1 index 2e12d5f..951cd20 100644 --- a/ScriptMessage/Public/Connect-ScriptMessage.ps1 +++ b/ScriptMessage/Public/Connect-ScriptMessage.ps1 @@ -36,13 +36,10 @@ function Connect-ScriptMessage ValueFromPipelineByPropertyName=$true)] [switch]$ReturnConnectionInfo ) - - # Set the necessary configuration variables. - $ScriptMessageConfig = Get-ScriptMessageConfig # Set the connection parameters. $ConnectionParameters = @{ - ServiceConfig = $ScriptMessageConfig.$Service + ServiceConfig = Get-ScriptMessageConfig -Service $Service } # Connect to the proper service. diff --git a/ScriptMessage/Services/MicrosoftGraph.ps1 b/ScriptMessage/Services/MicrosoftGraph.ps1 index b7aa72b..0cf5951 100644 --- a/ScriptMessage/Services/MicrosoftGraph.ps1 +++ b/ScriptMessage/Services/MicrosoftGraph.ps1 @@ -508,7 +508,11 @@ function Send-ScriptMessage_MicrosoftGraph ) # Set the Service ID. - $ServiceId = 'MicrosoftGraph' + # Keep this as a STRING and not the ENUM type since it's returned to the caller (functions will convert to [MessagingService] type as needed). + [string]$ServiceId = 'MicrosoftGraph' + + # Get the Service Config. + $ServiceConfig = Get-ScriptMessageConfig -Service $ServiceId # Send the message on each supported service specified. foreach ($typeItem in $Type) @@ -518,51 +522,64 @@ function Send-ScriptMessage_MicrosoftGraph 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)) + Mail { + # Check if AllowableMessageTypes contains '$typeItem'. + if ($ServiceConfig.AllowableMessageTypes -notcontains $typeItem) { - if ([string]::IsNullOrEmpty($Body.ContentType)) # Don't send 'ContentType' if not provided. It will default to 'Text' + $NewMessage = "The ScriptMessage configuration for '$ServiceId' does not allow sending messages of type: $typeItem" + Write-Warning -Message $NewMessage + $MgWarningMessages += "$NewMessage" + + # Set the 'SendEmailMessageResult (for return info on Status) to 'Error'. + $SendEmailMessageResult = 'Error' + } + else + { + # 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)) { - [hashtable]$Message['Body'] = ConvertTo-IMicrosoftGraphItemBody -Content $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 + } } - else - { - [hashtable]$Message['Body'] = ConvertTo-IMicrosoftGraphItemBody -Content $Body.Content -ContentType $Body.ContentType + [array]$Message['Attachment'] = ConvertTo-IMicrosoftGraphAttachment -Attachment $Attachment + + # TODO: Allow OPTION For Files To Be Shared Via OneDrive/SharePoint Instead Of As Direct Attachments. Perhaps add a switch parameter to override the default config. Maybe even have a config option of use OneDrive when over x Bytes. Set to 0 to always. + # 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 + } } - } - [array]$Message['Attachment'] = ConvertTo-IMicrosoftGraphAttachment -Attachment $Attachment - - # TODO: Allow OPTION For Files To Be Shared Via OneDrive/SharePoint Instead Of As Direct Attachments. Perhaps add a switch parameter to override the default config. Maybe even have a config option of use OneDrive when over x Bytes. Set to 0 to always. - # 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 # Note: This is correct as 'xxxx.Address' (not 'AddressObj'). It is converted Microsoft's to IMicrosoftGraphRecipient. } + + # Send Email. + $SendEmailMessageResult = Send-MgUserMail -UserId $SenderId -BodyParameter $EmailParams -PassThru } - - # Check For Separate 'SenderID' Value. Make equal to 'From' if not provided. - if ([string]::IsNullOrEmpty($SenderId)) - { - $SenderId = $Message.From.emailAddress.Address # Note: This is correct as 'xxxx.Address' (not 'AddressObj'). It is converted Microsoft's to IMicrosoftGraphRecipient. - } - - # Send Email. - $SendEmailMessageResult = Send-MgUserMail -UserId $SenderId -BodyParameter $EmailParams -PassThru # Collect Return Info $SendScriptMessageResult_SentFrom = [PSCustomObject]@{ @@ -641,304 +658,319 @@ function Send-ScriptMessage_MicrosoftGraph # 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') + Chat { # TODO MgChat: If application permissions, then do a bot message. Maybe for delegated give option of direct or bot message. + # Check if AllowableMessageTypes contains '$typeItem'. + if ($ServiceConfig.AllowableMessageTypes -notcontains $typeItem) { - $NewMessage = "Chat not sent. Microsoft Graph does not support sending Chat messages using Application permissions. Application permissions are only supported for migration into a Teams Channel." + $NewMessage = "The ScriptMessage configuration for '$ServiceId' does not allow sending messages of type: $typeItem" Write-Warning -Message $NewMessage $MgWarningMessages += "$NewMessage" + + # Set the 'SendChatMessageResult (for return info on Status) to 'Error'. + $SendChatMessageResult = 'Error' } else { - # Grab the latest MicrosoftGraph service context. - $MicrosoftGraphContext = Get-ScriptMessageContext -Service $ServiceId - - # Check For Separate 'SenderID' Value. Make equal to 'From' if not provided. - if ([string]::IsNullOrEmpty($SenderId)) + # Application CHAT permissions are only supported for migration into a Teams Channel. #TODO: TEST THIS AGAIN AFTER ALL THE CHANGES. MAKE SURE RETURN INFO IS OK. + if ($ServiceConfig.MgPermissionType -eq 'Application') { - $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 = "Chat not sent. Microsoft Graph does not support sending Chat messages on behalf of others." + $NewMessage = "Chat not sent. 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" + + # Set the 'SendChatMessageResult (for return info on Status) to 'Error'. + $SendChatMessageResult = 'Error' } else { - # 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}) + # Grab the latest MicrosoftGraph service context. + $MicrosoftGraphContext = Get-ScriptMessageContext -Service $ServiceId - if (($ChatType -eq [ChatType]'Group') -and ($IncludeBCCInGroupChat -eq $false)) - { - [array]$ChatRecipients = - $ChatRecipients_To + - $ChatRecipients_CC - } - else + # Check For Separate 'SenderID' Value. Make equal to 'From' if not provided. + if ([string]::IsNullOrEmpty($SenderId)) { - [array]$ChatRecipients = - $ChatRecipients_To + - $ChatRecipients_CC + - $ChatRecipients_BCC + $SenderId = $From.AddressObj } - - # Remove 'SenderID' address if it exists in the recipients list as well as duplicates. - # (Graph does not support sending direct chat messages to yourself since that's not a standard chat thread. I think it's some sort of "note" when used by Teams.) - [array]$ChatRecipients = $ChatRecipients | Sort-Object -Unique | Where-Object {$_ -ne $SenderId} - # Collect all chat participants. - [array]$AllChatParticipants = [array]$SenderId + [array]$ChatRecipients - - # Process chat only there are recipients. Otherwise warn if no chat recipients - if ($ChatRecipients.Count -eq 0) + # 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 = "Chat not sent. No chat recipients exist. If you are trying to send a chat message to yourself, please note that Microsoft doesn't support direct messaging to yourself via the Graph API." + $NewMessage = "Chat not sent. Microsoft Graph does not support sending Chat messages on behalf of others." Write-Warning -Message $NewMessage $MgWarningMessages += "$NewMessage" } else { - # Add a warning that BCC recipients (not in Sender, To, or CC) are not included in the group chat. + # 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)) { - 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" - } - } + [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. + # (Graph does not support sending direct chat messages to yourself since that's not a standard chat thread. I think it's some sort of "note" when used by Teams.) + [array]$ChatRecipients = $ChatRecipients | Sort-Object -Unique | Where-Object {$_ -ne $SenderId} + + # Collect all chat participants. + [array]$AllChatParticipants = [array]$SenderId + [array]$ChatRecipients - # 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)) + # Process chat only there are recipients. Otherwise warn if no chat recipients + if ($ChatRecipients.Count -eq 0) { - # Upload the attached file(s) to OneDrive. - $MgUserDrive = Get-MgUserDrive -UserId $($MicrosoftGraphContext.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) + $NewMessage = "Chat not sent. No chat recipients exist. If you are trying to send a chat message to yourself, please note that Microsoft doesn't support direct messaging to yourself via the Graph API." + Write-Warning -Message $NewMessage + $MgWarningMessages += "$NewMessage" + } + else + { + # 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)) { - $MicrosoftGraphDriveEndpointUri = '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. - try # Need to check if the $TeamsChatFolder exists first. If not, Get-MgDriveItem will throw a terminating exception. + foreach ($chatRecipient_BCC in $ChatRecipients_BCC) { - $ExistingFiles = (Get-MgDriveItem -DriveId $MgUserDrive.Id -DriveItemId $TeamsChatFolder -ExpandProperty 'Children').Children + 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" + } } - catch + } + + # 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 $($MicrosoftGraphContext.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) { - if ($_.Exception.Message -like "*itemNotFound*") + $MicrosoftGraphDriveEndpointUri = '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. + try # Need to check if the $TeamsChatFolder exists first. If not, Get-MgDriveItem will throw a terminating exception. { - $ExistingFiles = $null # Make sure this is null. + $ExistingFiles = (Get-MgDriveItem -DriveId $MgUserDrive.Id -DriveItemId $TeamsChatFolder -ExpandProperty 'Children').Children } - else + catch { - # Handle other types of errors - Write-Error "An unexpected error occurred while uploading file attachment for chat: $($_.Exception.Message)" + if ($_.Exception.Message -like "*itemNotFound*") + { + $ExistingFiles = $null # Make sure this is null. + } + else + { + # Handle other types of errors + Write-Error "An unexpected error occurred while uploading file attachment for chat: $($_.Exception.Message)" + } + } + + $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 = $($MicrosoftGraphDriveEndpointUri + $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 } - $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 + # 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 } - # Upload File # TODO: Test weird characters in filename like pound or something - $DriveItemId = "$TeamsChatFolder/$($AttachmentFileName):" - $InvokeUri = $($MicrosoftGraphDriveEndpointUri + $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) + # Convert Parameters to IMicrosoft* + $Message = @{} + if (-not [string]::IsNullOrEmpty($Body.Content)) { - $DriveInviteResult = Invoke-MgInviteDriveItem -DriveId $MgUserDrive.Id -DriveItemId $UploadDriveItemResult.id -BodyParameter $DriveInviteParams + 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) - # 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 + $ChatParams = [ordered]@{ + Body = $Message.Body + Attachments = $Message.Attachment + } } 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 + $ChatParams = [ordered]@{ + Body = $Message.Body + } } - } - # Create a new chat object, if needed, & send the message. - $Member_SenderID = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $SenderId) + # Create a new chat object, if needed, & send the message. + $Member_SenderID = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $SenderId) - switch ($ChatType) - { - OneOnOne + switch ($ChatType) { - foreach ($chatRecipient in $ChatRecipients) + 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. { - $Member_ChatRecipients = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $chatRecipient) + # Collect Group Members + [array]$Member_ChatRecipients = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $ChatRecipients) [array]$Message['Members'] = [array]$Member_SenderID + [array]$Member_ChatRecipients - try + + # 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 = $MicrosoftGraphContext | Select-Object -ExpandProperty Scopes + if (@($MicrosoftGraphScopes) -contains 'Chat.Read' -or @($MicrosoftGraphScopes) -contains 'Chat.ReadWrite') { - $NewChatResult = New-MgChat -ChatType $ChatType.ToString() -Members $Message.Members - $SendChatMessageResult = New-MgChatMessage -ChatId $NewChatResult.Id -BodyParameter $ChatParams + # 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 } - catch + else # Only has Chat.ReadBasic so we can't see the last message preview. { - $NewMessage = "Cannot create a chat with the recipient '$($chatRecipient)'." - Write-Warning -Message $NewMessage - $MgWarningMessages += "$NewMessage" + # 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 } - } - } - 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 = $MicrosoftGraphContext | 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)) + + # Reset the variable and then do a compare\search + $LatestExistingGroupChatMatch = $null + foreach ($existingGroupChat in $ExistingGroupChats) { - $LatestExistingGroupChatMatch = $existingGroupChat + 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 + # Send the chat message; create a new chat group if needed. + if (-not $LatestExistingGroupChatMatch) { - $NewChatResult = New-MgChat -ChatType $ChatType.ToString() -Members $Message.Members - $ChatToUse = $NewChatResult - $SendChatMessageResult = New-MgChatMessage -ChatId $ChatToUse.Id -BodyParameter $ChatParams + 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" + } } - catch + else { - $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" + $ChatToUse = $LatestExistingGroupChatMatch + $SendChatMessageResult = New-MgChatMessage -ChatId $ChatToUse.Id -BodyParameter $ChatParams } } - else - { - $ChatToUse = $LatestExistingGroupChatMatch - $SendChatMessageResult = New-MgChatMessage -ChatId $ChatToUse.Id -BodyParameter $ChatParams - } } } - } + } } - - # Collect Return Info - $SendScriptMessageResult_SentFrom = [PSCustomObject]@{ - Name = $From.Name - Address = $From.AddressObj + } + + # 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_To = foreach ($i in $To) + } + [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 doesn't have a method called Address) so we don't get the dreaded 'OverloadDefinitions'. + if ($null -ne $SendScriptMessageResult_Recipients_To) { - [PSCustomObject]@{ - Name = $i.Name - Address = $i.AddressObj - } + [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_To).Address } - [array]$SendScriptMessageResult_Recipients_CC = foreach ($i in $CC) + if ($null -ne $SendScriptMessageResult_Recipients_CC) { - [PSCustomObject]@{ - Name = $i.Name - Address = $i.AddressObj - } + [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_CC).Address } - [array]$SendScriptMessageResult_Recipients_BCC = foreach ($i in $BCC) + if ($null -ne $SendScriptMessageResult_Recipients_BCC) { - [PSCustomObject]@{ - Name = $i.Name - Address = $i.AddressObj - } + [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_BCC).Address } - [array]$SendScriptMessageResult_Recipients_All = @( # Since Address is also a PSMethod we need to do some fun stuff (List doesn't have a method called Address) so we don't get the dreaded 'OverloadDefinitions'. - if ($null -ne $SendScriptMessageResult_Recipients_To) - { - [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_To).Address - } - if ($null -ne $SendScriptMessageResult_Recipients_CC) - { - [System.Linq.Enumerable]::ToList([PSObject[]]$SendScriptMessageResult_Recipients_CC).Address - } - if ($null -ne $SendScriptMessageResult_Recipients_BCC) - { - [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 - } + ) + [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 From 3e6ecaf07cdd8e1c7b33299e6a27b0662348cd37 Mon Sep 17 00:00:00 2001 From: Sekers <46898253+Sekers@users.noreply.github.com> Date: Thu, 16 Oct 2025 20:34:34 -0500 Subject: [PATCH 11/15] FIX SENDING MULTIPLE FILE ATTACHMENTS IN EMAIL AND CHAT. The issue was that we were using [PSCustomObject] instead of a simple hashtable for each attachment item. --- ScriptMessage/Services/MicrosoftGraph.ps1 | 40 +++++++++++------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/ScriptMessage/Services/MicrosoftGraph.ps1 b/ScriptMessage/Services/MicrosoftGraph.ps1 index 0cf5951..2d2c636 100644 --- a/ScriptMessage/Services/MicrosoftGraph.ps1 +++ b/ScriptMessage/Services/MicrosoftGraph.ps1 @@ -107,10 +107,10 @@ Function ConvertTo-IMicrosoftGraphAttachment if (($currentAttachment.ContainsKey('Name')) -and $currentAttachment.ContainsKey('Content')) { $Attachment_ByteEncoded = [System.Convert]::ToBase64String($currentAttachment.Content) - [PSCustomObject]$IMicrosoftGraphAttachmentItem = @{ + $IMicrosoftGraphAttachmentItem = @{ "@odata.type" = "#microsoft.graph.fileAttachment" - Name = $currentAttachment.Name - ContentBytes = $Attachment_ByteEncoded + name = $currentAttachment.Name + contentBytes = $Attachment_ByteEncoded } $IMicrosoftGraphAttachmentItem } @@ -144,10 +144,10 @@ Function ConvertTo-IMicrosoftGraphChatMessageAttachment { if ($currentAttachment.ContainsKey('name') -and $currentAttachment.ContainsKey('webUrl')) { - [PSCustomObject]$IMicrosoftGraphChatMessageAttachmentItem = @{ - ContentType = 'reference' - ContentUrl = $currentAttachment.webUrl - Name = $currentAttachment.name + $IMicrosoftGraphChatMessageAttachmentItem = @{ + contentType = 'reference' + contentUrl = $currentAttachment.webUrl + name = $currentAttachment.name } $IMicrosoftGraphChatMessageAttachmentItem } @@ -806,20 +806,20 @@ function Send-ScriptMessage_MicrosoftGraph $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 + # Convert Parameters to IMicrosoft* + $Message = @{} + if (-not [string]::IsNullOrEmpty($Body.Content)) { - [hashtable]$Message['Body'] = ConvertTo-IMicrosoftGraphItemBody -Content $Body.Content -ContentType $Body.ContentType + 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) + $Message['Attachment'] = [array](ConvertTo-IMicrosoftGraphChatMessageAttachment -MgDriveItem $MgDriveItem) $ChatParams = [ordered]@{ Body = $Message.Body @@ -857,7 +857,7 @@ function Send-ScriptMessage_MicrosoftGraph } } } - Group #TODO: Sending more than one attachment causes no attachments to be included in the chat message. + Group { # Collect Group Members [array]$Member_ChatRecipients = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $ChatRecipients) From 864d255581a82b28c63299efa8c58e06239fbc11 Mon Sep 17 00:00:00 2001 From: Sekers <46898253+Sekers@users.noreply.github.com> Date: Thu, 16 Oct 2025 21:22:31 -0500 Subject: [PATCH 12/15] UPDATES TO ERROR CATCHING FOR MS GRAPH --- ScriptMessage/Services/MicrosoftGraph.ps1 | 518 ++++++++++++---------- 1 file changed, 272 insertions(+), 246 deletions(-) diff --git a/ScriptMessage/Services/MicrosoftGraph.ps1 b/ScriptMessage/Services/MicrosoftGraph.ps1 index 2d2c636..7a278bb 100644 --- a/ScriptMessage/Services/MicrosoftGraph.ps1 +++ b/ScriptMessage/Services/MicrosoftGraph.ps1 @@ -519,66 +519,73 @@ function Send-ScriptMessage_MicrosoftGraph { # Reset Warnings $MgWarningMessages = @() + $MgErrorMessages = @() switch ($typeItem) { Mail { - # Check if AllowableMessageTypes contains '$typeItem'. - if ($ServiceConfig.AllowableMessageTypes -notcontains $typeItem) + try { - $NewMessage = "The ScriptMessage configuration for '$ServiceId' does not allow sending messages of type: $typeItem" - Write-Warning -Message $NewMessage - $MgWarningMessages += "$NewMessage" - - # Set the 'SendEmailMessageResult (for return info on Status) to 'Error'. - $SendEmailMessageResult = 'Error' - } - else - { - # 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)) + # Check if AllowableMessageTypes contains '$typeItem'. + if ($ServiceConfig.AllowableMessageTypes -notcontains $typeItem) + { + $NewMessage = "The ScriptMessage configuration for '$ServiceId' does not allow sending messages of type: $typeItem" + Write-Warning -Message $NewMessage + $MgWarningMessages += "$NewMessage" + } + else { - if ([string]::IsNullOrEmpty($Body.ContentType)) # Don't send 'ContentType' if not provided. It will default to 'Text' + # 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)) { - [hashtable]$Message['Body'] = ConvertTo-IMicrosoftGraphItemBody -Content $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 + } } - else - { - [hashtable]$Message['Body'] = ConvertTo-IMicrosoftGraphItemBody -Content $Body.Content -ContentType $Body.ContentType + [array]$Message['Attachment'] = ConvertTo-IMicrosoftGraphAttachment -Attachment $Attachment + + # TODO: Allow OPTION For Files To Be Shared Via OneDrive/SharePoint Instead Of As Direct Attachments. Perhaps add a switch parameter to override the default config. Maybe even have a config option of use OneDrive when over x Bytes. Set to 0 to always. + # 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 + } } - } - [array]$Message['Attachment'] = ConvertTo-IMicrosoftGraphAttachment -Attachment $Attachment - - # TODO: Allow OPTION For Files To Be Shared Via OneDrive/SharePoint Instead Of As Direct Attachments. Perhaps add a switch parameter to override the default config. Maybe even have a config option of use OneDrive when over x Bytes. Set to 0 to always. - # 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 # Note: This is correct as 'xxxx.Address' (not 'AddressObj'). It is converted Microsoft's to IMicrosoftGraphRecipient. } + + # Send Email. + $SendEmailMessageResult = Send-MgUserMail -UserId $SenderId -BodyParameter $EmailParams -PassThru } - - # Check For Separate 'SenderID' Value. Make equal to 'From' if not provided. - if ([string]::IsNullOrEmpty($SenderId)) - { - $SenderId = $Message.From.emailAddress.Address # Note: This is correct as 'xxxx.Address' (not 'AddressObj'). It is converted Microsoft's to IMicrosoftGraphRecipient. - } - - # Send Email. - $SendEmailMessageResult = Send-MgUserMail -UserId $SenderId -BodyParameter $EmailParams -PassThru + } + catch + { + # Catch any errors and return as part of the $SendScriptMessageResult object. + $NewMessage = $_.Exception + $MgErrorMessages += "$NewMessage" } # Collect Return Info @@ -630,7 +637,7 @@ function Send-ScriptMessage_MicrosoftGraph } # Compile Caught Errors and Warnings - if ($MgWarningMessages.Count -gt 0) + if ($MgWarningMessages.Count -gt 0 -or $MgErrorMessages.Count -gt 0) { [array]$SendScriptMessageResult_Error = foreach ($mgWarningMessage in $MgWarningMessages) { @@ -639,6 +646,14 @@ function Send-ScriptMessage_MicrosoftGraph Message = $mgWarningMessage } } + + [array]$SendScriptMessageResult_Error += foreach ($mgErrorMessage in $MgErrorMessages) + { + [PSCustomObject]@{ + Type = 'Error' + Message = $mgErrorMessage + } + } } else { @@ -659,269 +674,272 @@ function Send-ScriptMessage_MicrosoftGraph $SendScriptMessageResult } Chat { # TODO MgChat: If application permissions, then do a bot message. Maybe for delegated give option of direct or bot message. - # Check if AllowableMessageTypes contains '$typeItem'. - if ($ServiceConfig.AllowableMessageTypes -notcontains $typeItem) + try { - $NewMessage = "The ScriptMessage configuration for '$ServiceId' does not allow sending messages of type: $typeItem" - Write-Warning -Message $NewMessage - $MgWarningMessages += "$NewMessage" - - # Set the 'SendChatMessageResult (for return info on Status) to 'Error'. - $SendChatMessageResult = 'Error' - } - else - { - # Application CHAT permissions are only supported for migration into a Teams Channel. #TODO: TEST THIS AGAIN AFTER ALL THE CHANGES. MAKE SURE RETURN INFO IS OK. - if ($ServiceConfig.MgPermissionType -eq 'Application') + # Check if AllowableMessageTypes contains '$typeItem'. + if ($ServiceConfig.AllowableMessageTypes -notcontains $typeItem) { - $NewMessage = "Chat not sent. Microsoft Graph does not support sending Chat messages using Application permissions. Application permissions are only supported for migration into a Teams Channel." + $NewMessage = "The ScriptMessage configuration for '$ServiceId' does not allow sending messages of type: $typeItem" Write-Warning -Message $NewMessage $MgWarningMessages += "$NewMessage" - - # Set the 'SendChatMessageResult (for return info on Status) to 'Error'. - $SendChatMessageResult = 'Error' } else { - # Grab the latest MicrosoftGraph service context. - $MicrosoftGraphContext = Get-ScriptMessageContext -Service $ServiceId - - # Check For Separate 'SenderID' Value. Make equal to 'From' if not provided. - if ([string]::IsNullOrEmpty($SenderId)) + # Application CHAT permissions are only supported for migration into a Teams Channel. #TODO: TEST THIS AGAIN AFTER ALL THE CHANGES. MAKE SURE RETURN INFO IS OK. + if ($ServiceConfig.MgPermissionType -eq 'Application') { - $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 = "Chat not sent. Microsoft Graph does not support sending Chat messages on behalf of others." + $NewMessage = "Chat not sent. 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" } else { - # 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}) + # Grab the latest MicrosoftGraph service context. + $MicrosoftGraphContext = Get-ScriptMessageContext -Service $ServiceId - if (($ChatType -eq [ChatType]'Group') -and ($IncludeBCCInGroupChat -eq $false)) - { - [array]$ChatRecipients = - $ChatRecipients_To + - $ChatRecipients_CC - } - else + # Check For Separate 'SenderID' Value. Make equal to 'From' if not provided. + if ([string]::IsNullOrEmpty($SenderId)) { - [array]$ChatRecipients = - $ChatRecipients_To + - $ChatRecipients_CC + - $ChatRecipients_BCC + $SenderId = $From.AddressObj } - - # Remove 'SenderID' address if it exists in the recipients list as well as duplicates. - # (Graph does not support sending direct chat messages to yourself since that's not a standard chat thread. I think it's some sort of "note" when used by Teams.) - [array]$ChatRecipients = $ChatRecipients | Sort-Object -Unique | Where-Object {$_ -ne $SenderId} - # Collect all chat participants. - [array]$AllChatParticipants = [array]$SenderId + [array]$ChatRecipients - - # Process chat only there are recipients. Otherwise warn if no chat recipients - if ($ChatRecipients.Count -eq 0) + # 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 = "Chat not sent. No chat recipients exist. If you are trying to send a chat message to yourself, please note that Microsoft doesn't support direct messaging to yourself via the Graph API." + $NewMessage = "Chat not sent. Microsoft Graph does not support sending Chat messages on behalf of others." Write-Warning -Message $NewMessage $MgWarningMessages += "$NewMessage" } else { - # Add a warning that BCC recipients (not in Sender, To, or CC) are not included in the group chat. + # 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)) { - foreach ($chatRecipient_BCC in $ChatRecipients_BCC) + [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. + # (Graph does not support sending direct chat messages to yourself since that's not a standard chat thread. I think it's some sort of "note" when used by Teams.) + [array]$ChatRecipients = $ChatRecipients | Sort-Object -Unique | Where-Object {$_ -ne $SenderId} + + # Collect all chat participants. + [array]$AllChatParticipants = [array]$SenderId + [array]$ChatRecipients + + # Process chat only there are recipients. Otherwise warn if no chat recipients + if ($ChatRecipients.Count -eq 0) + { + $NewMessage = "Chat not sent. No chat recipients exist. If you are trying to send a chat message to yourself, please note that Microsoft doesn't support direct messaging to yourself via the Graph API." + Write-Warning -Message $NewMessage + $MgWarningMessages += "$NewMessage" + } + else + { + # 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)) { - if ($chatRecipient_BCC -notin $AllChatParticipants) + foreach ($chatRecipient_BCC in $ChatRecipients_BCC) { - $NewMessage = "The following BCC recipient is not included in the group chat: $chatRecipient_BCC" - Write-Warning -Message $NewMessage - $MgWarningMessages += "$NewMessage" + 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 $($MicrosoftGraphContext.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) + # 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)) { - $MicrosoftGraphDriveEndpointUri = 'https://graph.microsoft.com/v1.0/drives/' - $AttachmentFileName = $attachmentItem.Name + # Upload the attached file(s) to OneDrive. + $MgUserDrive = Get-MgUserDrive -UserId $($MicrosoftGraphContext.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) + { + $MicrosoftGraphDriveEndpointUri = '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. + try # Need to check if the $TeamsChatFolder exists first. If not, Get-MgDriveItem will throw a terminating exception. + { + $ExistingFiles = (Get-MgDriveItem -DriveId $MgUserDrive.Id -DriveItemId $TeamsChatFolder -ExpandProperty 'Children').Children + } + catch + { + if ($_.Exception.Message -like "*itemNotFound*") + { + $ExistingFiles = $null # Make sure this is null. + } + else + { + # Handle other types of errors + Write-Error "An unexpected error occurred while uploading file attachment for chat: $($_.Exception.Message)" + } + } + + $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 = $($MicrosoftGraphDriveEndpointUri + $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 + } - # Get a list of existing files in the Teams Chat Files folder and rename if a file already exists with the same name. - try # Need to check if the $TeamsChatFolder exists first. If not, Get-MgDriveItem will throw a terminating exception. + # Update the file(s) sharing permissions. + $DriveInviteParams = ConvertTo-IMicrosoftGraphDriveInvite -EmailAddress $ChatRecipients + foreach ($UploadDriveItemResult in $MgDriveItem) { - $ExistingFiles = (Get-MgDriveItem -DriveId $MgUserDrive.Id -DriveItemId $TeamsChatFolder -ExpandProperty 'Children').Children + $DriveInviteResult = Invoke-MgInviteDriveItem -DriveId $MgUserDrive.Id -DriveItemId $UploadDriveItemResult.id -BodyParameter $DriveInviteParams } - catch + + # Convert Parameters to IMicrosoft* + $Message = @{} + if (-not [string]::IsNullOrEmpty($Body.Content)) { - if ($_.Exception.Message -like "*itemNotFound*") + if ([string]::IsNullOrEmpty($Body.ContentType)) # Don't send 'ContentType' if not provided. It will default to 'Text' { - $ExistingFiles = $null # Make sure this is null. + [hashtable]$Message['Body'] = ConvertTo-IMicrosoftGraphItemBody -Content $Body.Content } else { - # Handle other types of errors - Write-Error "An unexpected error occurred while uploading file attachment for chat: $($_.Exception.Message)" + [hashtable]$Message['Body'] = ConvertTo-IMicrosoftGraphItemBody -Content $Body.Content -ContentType $Body.ContentType } } - - $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 - } + $Message['Attachment'] = [array](ConvertTo-IMicrosoftGraphChatMessageAttachment -MgDriveItem $MgDriveItem) - # Upload File # TODO: Test weird characters in filename like pound or something - $DriveItemId = "$TeamsChatFolder/$($AttachmentFileName):" - $InvokeUri = $($MicrosoftGraphDriveEndpointUri + $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 + $ChatParams = [ordered]@{ + Body = $Message.Body + Attachments = $Message.Attachment + } } - - # Update the file(s) sharing permissions. - $DriveInviteParams = ConvertTo-IMicrosoftGraphDriveInvite -EmailAddress $ChatRecipients - foreach ($UploadDriveItemResult in $MgDriveItem) + else { - $DriveInviteResult = Invoke-MgInviteDriveItem -DriveId $MgUserDrive.Id -DriveItemId $UploadDriveItemResult.id -BodyParameter $DriveInviteParams + $ChatParams = [ordered]@{ + Body = $Message.Body + } } - # Convert Parameters to IMicrosoft* - $Message = @{} - if (-not [string]::IsNullOrEmpty($Body.Content)) + # Create a new chat object, if needed, & send the message. + $Member_SenderID = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $SenderId) + + switch ($ChatType) { - if ([string]::IsNullOrEmpty($Body.ContentType)) # Don't send 'ContentType' if not provided. It will default to 'Text' + OneOnOne { - [hashtable]$Message['Body'] = ConvertTo-IMicrosoftGraphItemBody -Content $Body.Content + 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" + } + } } - else + Group { - [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 - } - } + # Collect Group Members + [array]$Member_ChatRecipients = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $ChatRecipients) + [array]$Message['Members'] = [array]$Member_SenderID + [array]$Member_ChatRecipients - # Create a new chat object, if needed, & send the message. - $Member_SenderID = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $SenderId) + # See if a group chat already exists with the same recipients. + $MGChatProperties = @( + 'ChatType', + 'Id', + 'LastUpdatedDateTime' + ) - switch ($ChatType) - { - OneOnOne - { - foreach ($chatRecipient in $ChatRecipients) - { - $Member_ChatRecipients = [array](ConvertTo-IMicrosoftGraphConversationMember -EmailAddress $chatRecipient) - [array]$Message['Members'] = [array]$Member_SenderID + [array]$Member_ChatRecipients - try + # 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 = $MicrosoftGraphContext | Select-Object -ExpandProperty Scopes + if (@($MicrosoftGraphScopes) -contains 'Chat.Read' -or @($MicrosoftGraphScopes) -contains 'Chat.ReadWrite') { - $NewChatResult = New-MgChat -ChatType $ChatType.ToString() -Members $Message.Members - $SendChatMessageResult = New-MgChatMessage -ChatId $NewChatResult.Id -BodyParameter $ChatParams + # 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 } - catch + else # Only has Chat.ReadBasic so we can't see the last message preview. { - $NewMessage = "Cannot create a chat with the recipient '$($chatRecipient)'." - Write-Warning -Message $NewMessage - $MgWarningMessages += "$NewMessage" + # 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 } - } - } - Group - { - # 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 = $MicrosoftGraphContext | 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)) + + # Reset the variable and then do a compare\search + $LatestExistingGroupChatMatch = $null + foreach ($existingGroupChat in $ExistingGroupChats) { - $LatestExistingGroupChatMatch = $existingGroupChat + 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 + # Send the chat message; create a new chat group if needed. + if (-not $LatestExistingGroupChatMatch) { - $NewChatResult = New-MgChat -ChatType $ChatType.ToString() -Members $Message.Members - $ChatToUse = $NewChatResult - $SendChatMessageResult = New-MgChatMessage -ChatId $ChatToUse.Id -BodyParameter $ChatParams + 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" + } } - catch + else { - $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" + $ChatToUse = $LatestExistingGroupChatMatch + $SendChatMessageResult = New-MgChatMessage -ChatId $ChatToUse.Id -BodyParameter $ChatParams } } - else - { - $ChatToUse = $LatestExistingGroupChatMatch - $SendChatMessageResult = New-MgChatMessage -ChatId $ChatToUse.Id -BodyParameter $ChatParams - } } } - } - } + } + } } } + catch + { + # Catch any errors and return as part of the $SendScriptMessageResult object. + $NewMessage = $_.Exception + $MgErrorMessages += "$NewMessage" + } # Collect Return Info $SendScriptMessageResult_SentFrom = [PSCustomObject]@{ @@ -974,7 +992,7 @@ function Send-ScriptMessage_MicrosoftGraph } # Compile Caught Errors and Warnings - if ($MgWarningMessages.Count -gt 0) + if ($MgWarningMessages.Count -gt 0 -or $MgErrorMessages.Count -gt 0) { [array]$SendScriptMessageResult_Error = foreach ($mgWarningMessage in $MgWarningMessages) { @@ -983,6 +1001,14 @@ function Send-ScriptMessage_MicrosoftGraph Message = $mgWarningMessage } } + + [array]$SendScriptMessageResult_Error += foreach ($mgErrorMessage in $MgErrorMessages) + { + [PSCustomObject]@{ + Type = 'Error' + Message = $mgErrorMessage + } + } } else { From b5c51d5b6411eef119480df9d32b0bef0592d9fa Mon Sep 17 00:00:00 2001 From: Sekers <46898253+Sekers@users.noreply.github.com> Date: Thu, 16 Oct 2025 21:44:27 -0500 Subject: [PATCH 13/15] MORE MS GRAPH ERROR HANDLING TWEAKS --- ScriptMessage/Services/MicrosoftGraph.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ScriptMessage/Services/MicrosoftGraph.ps1 b/ScriptMessage/Services/MicrosoftGraph.ps1 index 7a278bb..27e5f51 100644 --- a/ScriptMessage/Services/MicrosoftGraph.ps1 +++ b/ScriptMessage/Services/MicrosoftGraph.ps1 @@ -584,7 +584,7 @@ function Send-ScriptMessage_MicrosoftGraph catch { # Catch any errors and return as part of the $SendScriptMessageResult object. - $NewMessage = $_.Exception + $NewMessage = $_ $MgErrorMessages += "$NewMessage" } @@ -937,7 +937,7 @@ function Send-ScriptMessage_MicrosoftGraph catch { # Catch any errors and return as part of the $SendScriptMessageResult object. - $NewMessage = $_.Exception + $NewMessage = $_ $MgErrorMessages += "$NewMessage" } From 3899a87c4204fa4f6069bdec3aa20b81d508a64d Mon Sep 17 00:00:00 2001 From: Sekers <46898253+Sekers@users.noreply.github.com> Date: Thu, 16 Oct 2025 22:21:33 -0500 Subject: [PATCH 14/15] ACCOMMODATE CHAT FILE UPLOADS IN MS GRAPH WITH NON-STANDARD FILENAMES --- ScriptMessage/Services/MicrosoftGraph.ps1 | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ScriptMessage/Services/MicrosoftGraph.ps1 b/ScriptMessage/Services/MicrosoftGraph.ps1 index 27e5f51..6854b0e 100644 --- a/ScriptMessage/Services/MicrosoftGraph.ps1 +++ b/ScriptMessage/Services/MicrosoftGraph.ps1 @@ -685,7 +685,7 @@ function Send-ScriptMessage_MicrosoftGraph } else { - # Application CHAT permissions are only supported for migration into a Teams Channel. #TODO: TEST THIS AGAIN AFTER ALL THE CHANGES. MAKE SURE RETURN INFO IS OK. + # Application CHAT permissions are only supported for migration into a Teams Channel. if ($ServiceConfig.MgPermissionType -eq 'Application') { $NewMessage = "Chat not sent. Microsoft Graph does not support sending Chat messages using Application permissions. Application permissions are only supported for migration into a Teams Channel." @@ -801,8 +801,11 @@ function Send-ScriptMessage_MicrosoftGraph $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):" + # Encode the filename to handle special characters in the filename when used in the URI. + $AttachmentFileName_Encoded = [System.Web.HttpUtility]::UrlEncode($AttachmentFileName) + + # Upload File + $DriveItemId = "$TeamsChatFolder/$($AttachmentFileName_Encoded):" $InvokeUri = $($MicrosoftGraphDriveEndpointUri + $MgUserDrive.Id + '/' + $DriveItemId + '/content') # Output the drive upload result. From d0745b93905e85a651e8a16c089eed8a4343d920 Mon Sep 17 00:00:00 2001 From: Sekers <46898253+Sekers@users.noreply.github.com> Date: Thu, 16 Oct 2025 22:40:22 -0500 Subject: [PATCH 15/15] UPDATES FOR RELEASE 1.1.0 --- CHANGELOG.md | 23 +++++++++++++++++++++ ScriptMessage/Public/Send-ScriptMessage.ps1 | 2 +- ScriptMessage/ScriptMessage.psd1 | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05a3819..e60e43e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog for ScriptMessage PowerShell Module +## [1.1.0](https://github.com/Sekers/ScriptMessage/tree/1.1.0) - (2025-10-16) + +### Fixes + +- ScriptMessage now properly allows for for NULL in TO, CC, BCC parameters in Microsoft Graph. +- Reverted recently added code formatting that included Begin\End process blocks as the way they were implemented caused bugs. +- Catch if a Teams Chat attachments folder does not already exists in the user's OneDrive. +- The 'AllowableMessageTypes' Microsoft Graph configuration is now being applied properly. + +### Features + +- Much better error handling with the Microsoft Graph service. Warnings and Errors are now collected and if one service type has an error the command will still process the other service type (when applicable). +- Get-ScriptMessageConfig can now optionally pull data for a specific service only. Returning all services configurations is still the default. +- Sending multiple attachments in a single email or chat now works with Microsoft Graph. +- Sending attachments with non-standard filenames (uploaded to the user's OneDrive) using Microsoft Graph Chat is now supported. + +### Other + +- Changes made to only import necessary Microsoft Graph PowerShell modules. + +Author: [**@Sekers**](https://github.com/Sekers) + +--- ## [1.0.8](https://github.com/Sekers/ScriptMessage/tree/1.0.8) - (2025-09-18) ### Fixes diff --git a/ScriptMessage/Public/Send-ScriptMessage.ps1 b/ScriptMessage/Public/Send-ScriptMessage.ps1 index 8fd7b2f..fa229d5 100644 --- a/ScriptMessage/Public/Send-ScriptMessage.ps1 +++ b/ScriptMessage/Public/Send-ScriptMessage.ps1 @@ -283,7 +283,7 @@ function Send-ScriptMessage } } - foreach ($serviceTypeObj in $ServiceType) + foreach ($serviceTypeObj in $ServiceType) # TODO: Catch errors once we have multiple services so if one fails the other(s) can still be processed. { # Set the connection parameters. $ConnectionParameters = @{ diff --git a/ScriptMessage/ScriptMessage.psd1 b/ScriptMessage/ScriptMessage.psd1 index 4493a57..3f67243 100644 --- a/ScriptMessage/ScriptMessage.psd1 +++ b/ScriptMessage/ScriptMessage.psd1 @@ -12,7 +12,7 @@ RootModule = 'ScriptMessage.psm1' # Version number of this module. -ModuleVersion = '1.0.8' +ModuleVersion = '1.1.0' # Supported PSEditions CompatiblePSEditions = @('Desktop','Core')