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