MSGraph.psm1
$script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\MSGraph.psd1").ModuleVersion # Detect whether at some level dotsourcing was enforced $script:doDotSource = Get-PSFConfigValue -FullName MSGraph.Import.DoDotSource -Fallback $false if ($MSGraph_dotsourcemodule) { $script:doDotSource = $true } <# Note on Resolve-Path: All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist. This is important when testing for paths. #> # Detect whether at some level loading individual module files, rather than the compiled module was enforced $importIndividualFiles = Get-PSFConfigValue -FullName MSGraph.Import.IndividualFiles -Fallback $false if ($MSGraph_importIndividualFiles) { $importIndividualFiles = $true } if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true } if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true } function Import-ModuleFile { <# .SYNOPSIS Loads files into the module on module import. .DESCRIPTION This helper function is used during module initialization. It should always be dotsourced itself, in order to proper function. This provides a central location to react to files being imported, if later desired .PARAMETER Path The path to the file to load .EXAMPLE PS C:\> . Import-ModuleFile -File $function.FullName Imports the file stored in $function according to import policy #> [CmdletBinding()] Param ( [string] $Path ) if ($doDotSource) { . (Resolve-Path $Path) } else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText((Resolve-Path $Path)))), $null, $null) } } #region Load individual files if ($importIndividualFiles) { # Execute Preimport actions . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1" # Import all internal functions foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Import all public functions foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Execute Postimport actions . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1" # End it here, do not load compiled code below return } #endregion Load individual files #region Load compiled code Add-Type -AssemblyName System.Net.Http Add-Type -AssemblyName System.Web Add-Type -AssemblyName System.Windows.Forms function Convert-UriQueryFromHash { <# .SYNOPSIS Converts hashtables to a string for REST api calls. .DESCRIPTION Converts hashtables to a string for REST api calls. .PARAMETER hash The hashtable to convert to a string .PARAMETER NoQuestionmark Supress the ? as the first character in the output string .EXAMPLE PS C:\> Convert-UriQueryFromHash -Hash @{ username = "user"; password = "password"} Converts the specified hashtable to the following string: ?password=password&username=user .EXAMPLE PS C:\> Convert-UriQueryFromHash -Hash @{ username = "user"; password = "password"} -NoQuestionmark Converts the specified hashtable to the following string: password=password&username=user #> [OutputType([System.String])] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true, Mandatory = $true)] [System.Collections.Hashtable] $Hash, [switch] $NoQuestionmark ) begin { } process { $elements = foreach ($key in $Hash.Keys) { $key + "=" + $Hash[$key] } $elementString = [string]::Join("&", $elements) if($NoQuestionMark) { "$elementString" } else { "?$elementString" } } end { } } function ConvertFrom-Base64StringWithNoPadding( [string]$Data ) { <# .SYNOPSIS Helper function build valid Base64 strings from JWT access tokens .DESCRIPTION Helper function build valid Base64 strings from JWT access tokens .PARAMETER Data The Token to convert .EXAMPLE PS C:\> ConvertFrom-Base64StringWithNoPadding -Data $data build valid base64 string the content from variable $data #> $Data = $Data.Replace('-', '+').Replace('_', '/') switch ($Data.Length % 4) { 0 { break } 2 { $Data += '==' } 3 { $Data += '=' } default { throw New-Object ArgumentException('data') } } [System.Convert]::FromBase64String($Data) } function ConvertFrom-JWTtoken { <# .SYNOPSIS Converts access tokens to readable objects .DESCRIPTION Converts access tokens to readable objects .PARAMETER Token The Token to convert .EXAMPLE PS C:\> ConvertFrom-JWTtoken -Token $Token Converts the content from variable $token to an object #> [cmdletbinding()] param( [Parameter(Mandatory = $true)] [string] $Token ) # Validate as per https://tools.ietf.org/html/rfc7519 - Access and ID tokens are fine, Refresh tokens will not work if ((-not $Token.Contains(".")) -or (-not $Token.StartsWith("eyJ"))) { Stop-PSFFunction -Message "Invalid data or not an access token" -EnableException -Tag JWT } # Split the token in its parts $tokenParts = $Token.Split(".") # Work on header $tokenHeader = [System.Text.Encoding]::UTF8.GetString( (ConvertFrom-Base64StringWithNoPadding $tokenParts[0]) ) $tokenHeaderJSON = $tokenHeader | ConvertFrom-Json # Work on payload $tokenPayload = [System.Text.Encoding]::UTF8.GetString( (ConvertFrom-Base64StringWithNoPadding $tokenParts[1]) ) $tokenPayloadJSON = $tokenPayload | ConvertFrom-Json # Work on signature $tokenSignature = ConvertFrom-Base64StringWithNoPadding $tokenParts[2] # Output $resultObject = New-Object MSGraph.Core.JWTAccessTokenInfo -Property @{ Header = $tokenHeader Payload = $tokenPayload Signature = $tokenSignature Algorithm = $tokenHeaderJSON.alg Type = $tokenHeaderJSON.typ ApplicationID = $tokenPayloadJSON.appid ApplicationName = $tokenPayloadJSON.app_displayname Audience = $tokenPayloadJSON.aud AuthenticationMethod = $tokenPayloadJSON.amr ExpirationTime = ([datetime]"1970-01-01Z00:00:00").AddSeconds($tokenPayloadJSON.exp).ToUniversalTime() GivenName = $tokenPayloadJSON.given_name IssuedAt = ([datetime]"1970-01-01Z00:00:00").AddSeconds($tokenPayloadJSON.iat).ToUniversalTime() Name = $tokenPayloadJSON.name NotBefore = ([datetime]"1970-01-01Z00:00:00").AddSeconds($tokenPayloadJSON.nbf).ToUniversalTime() OID = $tokenPayloadJSON.oid Plattform = $tokenPayloadJSON.platf Scope = $tokenPayloadJSON.scp SID = $tokenPayloadJSON.onprem_sid SourceIPAddr = $tokenPayloadJSON.ipaddr SureName = $tokenPayloadJSON.family_name TenantID = $tokenPayloadJSON.tid UniqueName = $tokenPayloadJSON.unique_name UPN = $tokenPayloadJSON.upn Version = $tokenPayloadJSON.ver } #$output $resultObject } function New-HttpClient { <# .SYNOPSIS Generates a HTTP Client for use with the Exchange Online Rest Api. .DESCRIPTION Generates a HTTP Client for use with the Exchange Online Rest Api. .PARAMETER MailboxName The mailbox to connect with. .EXAMPLE PS C:\> New-HttpClient -MailboxName 'foo@contoso.onmicrosoft.com' Creates a Http Client for connecting as 'foo@contoso.onmicrosoft.com' #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param () process { $handler = New-Object System.Net.Http.HttpClientHandler $handler.CookieContainer = New-Object System.Net.CookieContainer $handler.AllowAutoRedirect = $true $httpClient = New-Object System.Net.Http.HttpClient($handler) $header = New-Object System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json") $httpClient.DefaultRequestHeaders.Accept.Add($header) $httpClient.Timeout = New-Object System.TimeSpan(0, 0, 90) $httpClient.DefaultRequestHeaders.TransferEncodingChunked = $false $header = New-Object System.Net.Http.Headers.ProductInfoHeaderValue("RestClient", "1.1") $httpClient.DefaultRequestHeaders.UserAgent.Add($header) return $httpClient } } function Resolve-Token { <# .SYNOPSIS Test for specified Token, or receives registered token .DESCRIPTION Test for specified Token, or receives registered token. Helper function used for internal commands. .PARAMETER Token The Token to test and receive .PARAMETER FunctionName Name of the higher function which is calling this function. .EXAMPLE PS C:\> Resolve-Token -User $Token Test Token for lifetime, or receives registered token from script variable #> [OutputType([MSGraph.Core.AzureAccessToken])] [CmdletBinding()] param ( #[MSGraph.Core.AzureAccessToken] $Token, [String] $FunctionName ) if (-not $Token) { $Token = $script:msgraph_Token } if (-not $Token) { Stop-PSFFunction -Message "Not connected! Use New-MgaAccessToken to create a Token and either register it or specifs it" -EnableException $true -Category AuthenticationError -FunctionName $FunctionName } if ( (-not $Token.IsValid) -or ($Token.PercentRemaining -lt 15) ) { # if token is invalid or less then 15 percent of lifetime -> go and refresh the token $paramsTokenRefresh = @{ Token = $Token PassThru = $true } if ($script:msgraph_Token.AccessTokenInfo.Payload -eq $Token.AccessTokenInfo.Payload) { $paramsTokenRefresh.Add("Register", $true) } if ($Token.Credential) { $paramsTokenRefresh.Add("Credential", $Token.Credential) } $Token = Update-MgaAccessToken @paramsTokenRefresh } else { Write-PSFMessage -Level Verbose -Message "Valid token for user $($Token.UserprincipalName) - Time remaining $($Token.TimeRemaining)" -Tag "Authentication" } $Token } function Resolve-UserString { <# .SYNOPSIS Converts usernames or email addresses into the user targeting segment of the Rest Api call url. .DESCRIPTION Converts usernames or email addresses into the user targeting segment of the Rest Api call url. .PARAMETER User The user to convert .EXAMPLE PS C:\> Resolve-UserString -User $User Resolves $User into a legitimate user targeting string element. #> [OutputType([System.String])] [CmdletBinding()] param ( [string] $User ) if ($User -eq 'me' -or (-not $User)) { return 'me' } elseif ($User -like "users/*") { return $User } else { return "users/$($User)" } } function Show-OAuthWindow { <# .SYNOPSIS Generates a OAuth window for interactive authentication. .DESCRIPTION Generates a OAuth window for interactive authentication. .PARAMETER Url The url to the service offering authentication. .EXAMPLE PS C:\> Show-OAuthWindow -Url $uri Opens an authentication window to authenticate against the service pointed at in $uri #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Uri] $Url ) begin { $form = New-Object -TypeName System.Windows.Forms.Form -Property @{ Width = 440; Height = 640 } $web = New-Object -TypeName System.Windows.Forms.WebBrowser -Property @{ Width = 420; Height = 600; Url = ($url) } $docComp = { if ($web.Url.AbsoluteUri -match "error=[^&]*|code=[^&]*") { $form.Close() } } $web.ScriptErrorsSuppressed = $true $web.Add_DocumentCompleted($docComp) $form.Controls.Add($web) $form.Add_Shown({ $form.Activate() }) } process { $null = $form.ShowDialog() } end { $queryOutput = [System.Web.HttpUtility]::ParseQueryString($web.Url.Query) $output = @{ } foreach ($key in $queryOutput.Keys) { $output["$key"] = $queryOutput[$key] } [pscustomobject]$output } } function Get-MgaRegisteredAccessToken { <# .SYNOPSIS Output the registered access token .DESCRIPTION Output the registered access token .EXAMPLE PS C:\> Get-MgaRegisteredAccessToken Output the registered access token #> [CmdletBinding()] param () if ($script:msgraph_Token) { $script:msgraph_Token } else { Write-PSFMessage -Level Host -Message "No access token registered." } } function Invoke-MgaGetMethod { <# .SYNOPSIS Performs a rest GET against the graph API .DESCRIPTION Performs a rest GET against the graph API. Primarily used for internal commands. .PARAMETER Field The api child item under the username in the url of the api call. If this didn't make sense to you, you probably shouldn't be using this command ;) .PARAMETER User The user to execute this under. Defaults to the user the token belongs to. .PARAMETER Delta Indicates that the query is intend to be a delta query, so a delta-link property is added to the output-object ('@odata.deltaLink'). .PARAMETER DeltaLink Specifies the uri to query for delta objects on a query. .PARAMETER ResultSize The user to execute this under. Defaults to the user the token belongs to. .PARAMETER Token The access token to use to connect. .PARAMETER FunctionName Name of the higher function which is calling this function. .EXAMPLE PS C:\> Invoke-MgaGetMethod -Field 'mailFolders' -Token $Token -User $User Retrieves a list of email folders for the user $User, using the token stored in $Token #> [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [string] $Field, [string] $User, [Parameter(ParameterSetName = 'Default')] [switch] $Delta, [Parameter(ParameterSetName = 'DeltaLink')] [string] $DeltaLink, [Int64] $ResultSize = (Get-PSFConfigValue -FullName 'MSGraph.Query.ResultSize' -Fallback 100), [MSGraph.Core.AzureAccessToken] $Token, [string] $FunctionName = $MyInvocation.MyCommand ) #region variable definition $Token = Resolve-Token -Token $Token -FunctionName $FunctionName if($PSCmdlet.ParameterSetName -like "DeltaLink") { Write-PSFMessage -Level VeryVerbose -Message "ParameterSet $($PSCmdlet.ParameterSetName) - constructing delta query" -Tag "ParameterSetHandling" $restUri = $DeltaLink $Delta = $true $User = ([uri]$restUri).AbsolutePath.split('/')[2] } else { if(-not $User) { $User = $Token.UserprincipalName } $restUri = "https://graph.microsoft.com/v1.0/$(Resolve-UserString -User $User)/$($Field)" if($Delta) { $restUri = $restUri + "/delta" } } if ($ResultSize -eq 0) { $ResultSize = [Int64]::MaxValue } #if ($ResultSize -le 10 -and $restUri -notmatch '\$top=') { $restUri = $restUri + "?`$top=$($ResultSize)" } [Int64]$i = 0 [Int64]$overResult = 0 $tooManyItems = $false $output = @() #endregion variable definition #region query data do { Write-PSFMessage -Tag "RestData" -Level VeryVerbose -Message "Get REST data: $($restUri)" Clear-Variable -Name data -Force -WhatIf:$false -Confirm:$false -Verbose:$false -ErrorAction Ignore $invokeParam = @{ Method = "Get" Uri = $restUri Headers = @{ "Authorization" = "Bearer $( [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($token.AccessToken)) )" "Content-Type" = "application/json" } } $data = Invoke-RestMethod @invokeParam -ErrorVariable "restError" -Verbose:$false -UseBasicParsing if($restError) { Stop-PSFFunction -Tag "RestData" -Message $parseError[0].Exception -Exception $parseError[0].Exception -EnableException $false -Category ConnectionError -FunctionName $FunctionName return } if("Value" -in $data.psobject.Properties.Name) { # Multi object with value property returned by api call [array]$value = $data.Value Write-PSFMessage -Tag "RestData" -Level VeryVerbose -Message "Retrieving $($value.Count) records from query" $i = $i + $value.Count if($i -lt $ResultSize) { $restUri = $data.'@odata.nextLink' } else { $restUri = "" $tooManyItems = $true $overResult = $ResultSize - ($i - $value.Count) Write-PSFMessage -Tag "ResultSize" -Level Verbose -Message "Resultsize ($ResultSize) exeeded. Output $($overResult) object(s) in record set." } } else { # Multi object with value property returned by api call Write-PSFMessage -Tag "RestData" -Level VeryVerbose -Message "Single item retrived. Outputting data." [array]$value = $data $restUri = "" } if((-not $tooManyItems) -or ($overResult -gt 0)) { # check if resultsize is reached if($overResult -gt 0) { $output = $output + $Value[0..($overResult-1)] } else { $output = $output + $Value } } } while ($restUri) #endregion query data #region output data $output | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -Force if($Delta) { if('@odata.deltaLink' -in $data.psobject.Properties.Name) { $output | Add-Member -MemberType NoteProperty -Name '@odata.deltaLink' -Value $data.'@odata.deltaLink' -PassThru } else { $output | Add-Member -MemberType NoteProperty -Name '@odata.deltaLink' -Value $data.'@odata.nextLink' -PassThru } } else { $output } if($tooManyItems) { # write information to console if resultsize exceeds if($Delta) { Write-PSFMessage -Tag "GetData" -Level Host -Message "Reaching maximum ResultSize before finishing delta query. Next delta query will continue on pending objects. Current ResultSize: $($ResultSize)" -FunctionName $FunctionName } else { Write-PSFMessage -Tag "GetData" -Level Warning -Message "Too many items. Reaching maximum ResultSize before finishing query. You may want to increase the ResultSize. Current ResultSize: $($ResultSize)" -FunctionName $FunctionName } } #endregion output data } function Invoke-MgaPatchMethod { <# .SYNOPSIS Performs a REST PATCH against the graph API .DESCRIPTION Performs a REST PATCH against the graph API. Primarily used for internal commands. .PARAMETER Field The api child item under the username in the url of the api call. If this didn't make sense to you, you probably shouldn't be using this command ;) .PARAMETER User The user to execute this under. Defaults to the user the token belongs to. .PARAMETER Body JSON date as string to send as body on the REST call .PARAMETER ContentType Nature of the data in the body of an entity. Required. .PARAMETER Token The access token to use to connect. .PARAMETER FunctionName Name of the higher function which is calling this function. .EXAMPLE PS C:\> Invoke-MgaPatchMethod -Field "messages/$($id)" -Body '{ "isRead": true }' -Token $Token Set a message as readed. The token stored in $Token is used for the api call. #> [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [string] $Field, [string] $User, [String] $Body, [ValidateSet("application/json")] [String] $ContentType = "application/json", [MSGraph.Core.AzureAccessToken] $Token, [string] $FunctionName = $MyInvocation.MyCommand ) $Token = Resolve-Token -Token $Token -FunctionName $FunctionName if (-not $User) { $User = $Token.UserprincipalName } $restUri = "https://graph.microsoft.com/v1.0/$(Resolve-UserString -User $User)/$($Field)" Write-PSFMessage -Tag "RestData" -Level VeryVerbose -Message "Invoking REST PATCH to uri: $($restUri)" Write-PSFMessage -Tag "RestData" -Level Debug -Message "REST body data: $($Body)" Clear-Variable -Name data -Force -WhatIf:$false -Confirm:$false -Verbose:$false -ErrorAction Ignore $invokeParam = @{ Method = "Patch" Uri = $restUri Body = $Body Headers = @{ "Authorization" = "Bearer $( [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($token.AccessToken)) )" "Content-Type" = "application/json" } } $data = Invoke-RestMethod @invokeParam -ErrorVariable "restError" -Verbose:$false -UseBasicParsing if ($restError) { Stop-PSFFunction -Tag "RestData" -Message $parseError[0].Exception -Exception $parseError[0].Exception -EnableException $false -Category ConnectionError -FunctionName $FunctionName return } $data | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -Force $data } function Invoke-MgaPostMethod { <# .SYNOPSIS Performs a REST POST against the graph API .DESCRIPTION Performs a REST POST against the graph API. Primarily used for internal commands. .PARAMETER Field The api child item under the username in the url of the api call. If this didn't make sense to you, you probably shouldn't be using this command ;) .PARAMETER User The user to execute this under. Defaults to the user the token belongs to. .PARAMETER Body JSON date as string to send as body on the REST call .PARAMETER ContentType Nature of the data in the body of an entity. Required. .PARAMETER Token The access token to use to connect. .PARAMETER FunctionName Name of the higher function which is calling this function. .EXAMPLE PS C:\> Invoke-MgaPostMethod -Field "messages/$($id)/reply" -Body '{"comment": "comment-value"}' -Token $Token Reply to the sender of a message with the id, stored in variable $id. The message is then saved in the Sent Items folder. The token stored in $Token is used for the api call. #> [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [string] $Field, [string] $User, [String] $Body, [ValidateSet("application/json")] [String] $ContentType = "application/json", [MSGraph.Core.AzureAccessToken] $Token, [string] $FunctionName = $MyInvocation.MyCommand ) $Token = Resolve-Token -Token $Token -FunctionName $FunctionName if (-not $User) { $User = $Token.UserprincipalName } $restUri = "https://graph.microsoft.com/v1.0/$(Resolve-UserString -User $User)/$($Field)" Write-PSFMessage -Tag "RestData" -Level VeryVerbose -Message "Invoking REST POST to uri: $($restUri)" Write-PSFMessage -Tag "RestData" -Level Debug -Message "REST body data: $($Body)" Clear-Variable -Name data -Force -WhatIf:$false -Confirm:$false -Verbose:$false -ErrorAction Ignore $invokeParam = @{ Method = "Post" Uri = $restUri Body = $Body Headers = @{ "Authorization" = "Bearer $( [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($token.AccessToken)) )" "Content-Type" = "application/json" } } $data = Invoke-RestMethod @invokeParam -ErrorVariable "restError" -Verbose:$false -UseBasicParsing if ($restError) { Stop-PSFFunction -Tag "RestData" -Message $parseError[0].Exception -Exception $parseError[0].Exception -EnableException $false -Category ConnectionError -FunctionName $FunctionName return } $data | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -Force $data } function New-MgaAccessToken { <# .SYNOPSIS Creates an access token for contacting the specified application endpoint .DESCRIPTION Creates an access token for contacting the specified application endpoint .PARAMETER MailboxName The email address of the mailbox to access .PARAMETER Credential The credentials to use to authenticate the request. Using this avoids the need to visually interact with the logon screen. Only works for accounts that have once logged in visually, but can be used from any machine. .PARAMETER ClientId The ID of the client to connect with. This is the ID of the registered application. .PARAMETER RedirectUrl Some weird vodoo. Leave it as it is, unless you know better .PARAMETER Refresh Try to do a refresh login dialag, which may possibly avoid entering password again. .PARAMETER Register Registers the token, so all subsequent calls to Exchange Online reuse it by default. .PARAMETER PassThru Outputs the token to the console, even when the register switch is set .EXAMPLE PS C:\> New-MgaAccessToken -MailboxName 'max.musterman@contoso.com' Registers an application to run under 'max.mustermann@contoso.com'. Requires an interactive session with a user handling the web UI. .EXAMPLE PS C:\> New-MgaAccessToken -MailboxName 'max.musterman@contoso.com' -Credential $cred Generates a token to a session as max.mustermann@contoso.com under the credentials specified in $cred. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(DefaultParameterSetName="Default")] param ( [PSCredential] $Credential, [System.Guid] $ClientId = (Get-PSFConfigValue -FullName MSGraph.Tenant.Application.ClientID -NotNull), [string] $RedirectUrl = (Get-PSFConfigValue -FullName MSGraph.Tenant.Application.RedirectUrl -Fallback "urn:ietf:wg:oauth:2.0:oob"), [switch] $Refresh, [Parameter(ParameterSetName='Register')] [switch] $Register, [Parameter(ParameterSetName='Register')] [switch] $PassThru ) # variable definitions $resourceUri = "https://graph.microsoft.com" $baselineTimestamp = [datetime]"1970-01-01Z00:00:00" $endpointUri = "https://login.windows.net/common/oauth2" $endpointUriAuthorize = "$($endpointUri)/authorize" $endpointUriToken = "$($endpointUri)/token " # Creating http client for logon $httpClient = New-HttpClient if (-not $Credential) { # Request an authorization code with web form # Info https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-protocols-oauth-code#request-an-authorization-code Write-PSFMessage -Level Verbose -Message "Authentication is done by code. Query authentication from login form." -Tag "Authorization" $queryHash = [ordered]@{ resource = [System.Web.HttpUtility]::UrlEncode($resourceUri) client_id = "$($ClientId)" response_type = "code" redirect_uri = [System.Web.HttpUtility]::UrlEncode($redirectUrl) } if($Refresh) { $queryHash.Add("prompt","refresh_session") } $phase1auth = Show-OAuthWindow -Url ($endpointUriAuthorize + (Convert-UriQueryFromHash $queryHash)) # build authorization string with authentication code from web form auth $queryHash = [ordered]@{ resource = [System.Web.HttpUtility]::UrlEncode($resourceUri) client_id = "$($ClientId)" grant_type = "authorization_code" code = "$($phase1auth.code)" redirect_uri = "$($redirectUrl)" } $authorizationPostRequest = Convert-UriQueryFromHash $queryHash -NoQuestionmark } else { # build authorization string with plain text credentials # Info https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-oauth2-client-creds-grant-flow#request-an-access-token Write-PSFMessage -Level Verbose -Message "Authentication is done by specified credentials. (No TwoFactor-Authentication supported!)" -Tag "Authorization" $queryHash = [ordered]@{ resource = [System.Web.HttpUtility]::UrlEncode($resourceUri) client_id = $ClientId grant_type = "password" username = $Credential.UserName password = $Credential.GetNetworkCredential().password } $authorizationPostRequest = Convert-UriQueryFromHash $queryHash -NoQuestionmark } # Request an access token $content = New-Object System.Net.Http.StringContent($authorizationPostRequest, [System.Text.Encoding]::UTF8, "application/x-www-form-urlencoded") $clientResult = $httpClient.PostAsync([Uri]($endpointUriToken), $content) if($clientResult.Result.StatusCode -eq [System.Net.HttpStatusCode]"OK") { Write-PSFMessage -Level Verbose -Message "AccessToken granted. $($clientResult.Result.StatusCode.value__) ($($clientResult.Result.StatusCode)) $($clientResult.Result.ReasonPhrase)" -Tag "Authorization" } else { Stop-PSFFunction -Message "Request for AccessToken failed. $($clientResult.Result.StatusCode.value__) ($($clientResult.Result.StatusCode)) $($clientResult.Result.ReasonPhrase)" -Tag "Authorization" -EnableException $true } $jsonResponse = ConvertFrom-Json -InputObject $clientResult.Result.Content.ReadAsStringAsync().Result # Build output object $resultObject = New-Object MSGraph.Core.AzureAccessToken -Property @{ TokenType = $jsonResponse.token_type Scope = $jsonResponse.scope -split " " ValidUntilUtc = $baselineTimestamp.AddSeconds($jsonResponse.expires_on).ToUniversalTime() ValidFromUtc = $baselineTimestamp.AddSeconds($jsonResponse.not_before).ToUniversalTime() ValidUntil = New-Object DateTime($baselineTimestamp.AddSeconds($jsonResponse.expires_on).Ticks) ValidFrom = New-Object DateTime($baselineTimestamp.AddSeconds($jsonResponse.not_before).Ticks) AccessToken = $null RefreshToken = $null IDToken = $null Credential = $Credential ClientId = $ClientId Resource = $resourceUri AppRedirectUrl = $RedirectUrl } # Insert token data into output object. done as secure string to prevent text output of tokens if ($jsonResponse.psobject.Properties.name -contains "refresh_token") { $resultObject.RefreshToken = ($jsonResponse.refresh_token | ConvertTo-SecureString -AsPlainText -Force) } if ($jsonResponse.psobject.Properties.name -contains "id_token") { $resultObject.IDToken = ($jsonResponse.id_token | ConvertTo-SecureString -AsPlainText -Force) } if ($jsonResponse.psobject.Properties.name -contains "access_token") { $resultObject.AccessToken = ($jsonResponse.access_token | ConvertTo-SecureString -AsPlainText -Force) $resultObject.AccessTokenInfo = ConvertFrom-JWTtoken -Token $jsonResponse.access_token } if ((Get-Date).IsDaylightSavingTime()) { $resultObject.ValidUntil = $resultObject.ValidUntil.AddHours(1) $resultObject.ValidFrom = $resultObject.ValidFrom.AddHours(1) } if($resultObject.IsValid) { if ($Register) { $script:msgraph_Token = $resultObject if($PassThru) { $resultObject } } else { $resultObject } } else { Stop-PSFFunction -Message "Token failure. Acquired token is not valid" -EnableException -Tag "Authorization" } } function Register-MgaAccessToken { <# .SYNOPSIS Registers an access token .DESCRIPTION Registers an access token, so all subsequent calls to Exchange Online reuse it by default. .PARAMETER Token The Token to register as default token for subsequent calls. .PARAMETER PassThru Outputs the token to the console .EXAMPLE PS C:\> Get-MgaRegisteredAccessToken Output the registered access token #> [CmdletBinding (SupportsShouldProcess=$false, ConfirmImpact='Medium')] [OutputType([MSGraph.Core.AzureAccessToken])] param ( [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ValueFromRemainingArguments=$false)] [ValidateNotNullOrEmpty()] #[MSGraph.Core.AzureAccessToken] $Token, [switch] $PassThru ) $script:msgraph_Token = $Token if($PassThru) { $script:msgraph_Token } } function Update-MgaAccessToken { <# .SYNOPSIS Updates an existing access token .DESCRIPTION Updates an existing access token for contacting the specified application endpoint as long as the token is still valid. Otherwise, a new access is called through New-MgaAccessToken. .PARAMETER Token The token object to renew. .PARAMETER Register Registers the renewed token, so all subsequent calls to Exchange Online reuse it by default. .PARAMETER PassThru Outputs the token to the console, even when the register switch is set .EXAMPLE PS C:\> New-MgaAccessToken -MailboxName 'max.musterman@contoso.com' Registers an application to run under 'max.mustermann@contoso.com'. Requires an interactive session with a user handling the web UI. .EXAMPLE PS C:\> New-MgaAccessToken -MailboxName 'max.musterman@contoso.com' -Credential $cred Generates a token to a session as max.mustermann@contoso.com under the credentials specified in $cred. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(DefaultParameterSetName="Default")] param ( [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] #[MSGraph.Core.AzureAccessToken] $Token, [Parameter(ParameterSetName='Register')] [switch] $Register, [Parameter(ParameterSetName='Register')] [switch] $PassThru ) begin { } process { if (-not $Token) { $Token = $script:msgraph_Token $Register = $true } if (-not $Token) { Stop-PSFFunction -Message "Not connected! Use New-MgaAccessToken to create a Token and either register it or specifs it." -EnableException $true -Category AuthenticationError -Cmdlet $PSCmdlet } if (-not $Token.IsValid) { Write-PSFMessage -Level Warning -Message "Token lifetime already expired and can't be newed. New authentication is required. Calling New-MgaAccessToken..." -Tag "Authorization" $paramsNewToken = @{ ClientId = $Token.AccessTokenInfo.ApplicationID.Guid RedirectUrl = $Token.AppRedirectUrl } if ($Token.Credential) { $paramsNewToken.Add("Credential", $Token.Credential ) } if ($Register -or ($script:msgraph_Token.AccessTokenInfo.Payload -eq $Token.AccessTokenInfo.Payload) ) { $paramsNewToken.Add("Register", $true) } $resultObject = New-MgaAccessToken -PassThru @paramsNewToken if ($PassThru) { return $resultObject } else { return } } $resourceUri = "https://graph.microsoft.com" $endpointUri = "https://login.windows.net/common/oauth2" $endpointUriToken = "$($endpointUri)/token " $baselineTimestamp = [datetime]"1970-01-01Z00:00:00" $httpClient = New-HttpClient $queryHash = [ordered]@{ grant_type = "refresh_token" resource = [System.Web.HttpUtility]::UrlEncode($resourceUri) client_id = $Token.ClientId.Guid refresh_token = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Token.RefreshToken)) } $authorizationPostRequest = Convert-UriQueryFromHash $queryHash -NoQuestionmark $content = New-Object System.Net.Http.StringContent($authorizationPostRequest, [System.Text.Encoding]::UTF8, "application/x-www-form-urlencoded") $clientResult = $httpClient.PostAsync([Uri]$endpointUriToken, $content) if ($clientResult.Result.StatusCode -eq [System.Net.HttpStatusCode]"OK") { Write-PSFMessage -Level Verbose -Message "AccessToken renewal successful. $($clientResult.Result.StatusCode.value__) ($($clientResult.Result.StatusCode)) $($clientResult.Result.ReasonPhrase)" -Tag "Authorization" } else { Stop-PSFFunction -Message "Failed to renew AccessToken! $($clientResult.Result.StatusCode.value__) ($($clientResult.Result.StatusCode)) $($clientResult.Result.ReasonPhrase)" -Tag "Authorization" -EnableException $true } $jsonResponse = ConvertFrom-Json -InputObject $clientResult.Result.Content.ReadAsStringAsync().Result # Build output object $resultObject = New-Object MSGraph.Core.AzureAccessToken -Property @{ TokenType = $jsonResponse.token_type Scope = $jsonResponse.scope -split " " ValidUntilUtc = $baselineTimestamp.AddSeconds($jsonResponse.expires_on).ToUniversalTime() ValidFromUtc = $baselineTimestamp.AddSeconds($jsonResponse.not_before).ToUniversalTime() ValidUntil = New-Object DateTime($baselineTimestamp.AddSeconds($jsonResponse.expires_on).Ticks) ValidFrom = New-Object DateTime($baselineTimestamp.AddSeconds($jsonResponse.not_before).Ticks) AccessToken = $null RefreshToken = $null IDToken = $null Credential = $Token.Credential ClientId = $Token.ClientId.Guid Resource = $Token.Resource.ToString() AppRedirectUrl = $Token.AppRedirectUrl.ToString() } # Insert token data into output object. done as secure string to prevent text output of tokens if ($jsonResponse.psobject.Properties.name -contains "refresh_token") { $resultObject.RefreshToken = ($jsonResponse.refresh_token | ConvertTo-SecureString -AsPlainText -Force) } if ($jsonResponse.psobject.Properties.name -contains "id_token") { $resultObject.IDToken = ($jsonResponse.id_token | ConvertTo-SecureString -AsPlainText -Force) } if ($jsonResponse.psobject.Properties.name -contains "access_token") { $resultObject.AccessToken = ($jsonResponse.access_token | ConvertTo-SecureString -AsPlainText -Force) $resultObject.AccessTokenInfo = ConvertFrom-JWTtoken -Token $jsonResponse.access_token } if ((Get-Date).IsDaylightSavingTime()) { $resultObject.ValidUntil = $resultObject.ValidUntil.AddHours(1) $resultObject.ValidFrom = $resultObject.ValidFrom.AddHours(1) } if ($resultObject.IsValid) { if ($Register) { $script:msgraph_Token = $resultObject if ($PassThru) { $resultObject } } else { $resultObject } } else { Stop-PSFFunction -Message "Token failure. Acquired token is not valid" -EnableException -Tag "Authorization" } } end { } } function Export-MgaMailAttachment { <# .SYNOPSIS Export a mail attachment to a file .DESCRIPTION Export/saves a mail attachment to a file .PARAMETER Path The directory where to export the attachment .PARAMETER InputObject The attachment object to export .EXAMPLE PS C:\> Export-MgaMailAttachment -InputObject $attachment -Path "$HOME" Export the attement to the users profile base directory #> [CmdletBinding()] [Alias('Save-MgaMailAttachment')] param ( [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ValueFromRemainingArguments=$false)] [ValidateNotNullOrEmpty()] [MSGraph.Exchange.Mail.Attachment] $InputObject, [String] $Path ) begin { if (Test-Path -Path $Path -IsValid) { if (-not (Test-Path -Path $Path -PathType Container)) { Stop-PSFFunction -Message "Specified path is a file and not a path. Please specify a directory." -EnableException $true -Category "InvalidPath" -Tag "Attachment" } } else { Stop-PSFFunction -Message "Specified path is not valid. Please specify a valid directory." -EnableException $true -Category "InvalidPath" -Tag "Attachment" } $Path = Resolve-Path -Path $Path } process { foreach ($attachment in $InputObject.BaseObject) { [system.convert]::FromBase64String($attachment.contentBytes) | Set-Content -Path (Join-Path -Path $Path -ChildPath $attachment.Name) -Encoding Byte } } end { } } function Get-MgaMailAttachment { <# .SYNOPSIS Retrieves the attachment object from a email message in Exchange Online using the graph api. .DESCRIPTION Retrieves the attachment object from a email message in Exchange Online using the graph api. .PARAMETER MailId The display name of the folder to search. Defaults to the inbox. .PARAMETER User The user-account to access. Defaults to the main user connected as. Can be any primary email name of any user the connected token has access to. .PARAMETER IncludeInlineAttachment This will retrieve also attachments like pictures in the html body of the mail. .PARAMETER ResultSize The user to execute this under. Defaults to the user the token belongs to. .PARAMETER Token The token representing an established connection to the Microsoft Graph Api. Can be created by using New-EORAccessToken. Can be omitted if a connection has been registered using the -Register parameter on New-EORAccessToken. .EXAMPLE PS C:\> Get-MgaMailMessage Return all emails in the inbox of the user connected to through a token #> [CmdletBinding()] [OutputType([MSGraph.Exchange.Mail.Attachment])] param ( [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName='ById')] [Alias('Id')] [string[]] $MailId, [Parameter(ParameterSetName='ById')] [string] $User = 'me', [switch] $IncludeInlineAttachment, [Int64] $ResultSize = (Get-PSFConfigValue -FullName 'MSGraph.Query.ResultSize' -Fallback 100), [MSGraph.Core.AzureAccessToken] $Token ) begin { #[Parameter(ValueFromPipeline = $true, ParameterSetName='ByInputObject')] #[Alias('Mail', 'MailMessage', 'Message')] #[MSGraph.Exchange.Mail.Message] #$InputObject, } process { foreach ($mail in $MailId) { Write-PSFMessage -Level Verbose -Message "Getting attachment from mail" $data = Invoke-MgaGetMethod -Field "messages/$($mail)/attachments" -User $User -Token $Token -ResultSize $ResultSize if(-not $IncludeInlineAttachment) { $data = $data | Where-Object isInline -eq $false} foreach ($output in $data) { [MSGraph.Exchange.Mail.Attachment]@{ BaseObject = $output } } } } end { } } function Get-MgaMailFolder { <# .SYNOPSIS Searches mail folders in Exchange Online .DESCRIPTION Searches mail folders in Exchange Online .PARAMETER Name The name of the folder(S) to query. .PARAMETER IncludeChildFolders Output all subfolders on queried folder(s). .PARAMETER Recurse Iterates through the whole folder structure and query all subfolders. .PARAMETER Filter The name to filter by. (Client Side filtering) .PARAMETER User The user-account to access. Defaults to the main user connected as. Can be any primary email name of any user the connected token has access to. .PARAMETER ResultSize The user to execute this under. Defaults to the user the token belongs to. .PARAMETER Token The token representing an established connection to the Microsoft Graph Api. Can be created by using New-EORAccessToken. Can be omitted if a connection has been registered using the -Register parameter on New-EORAccessToken. .EXAMPLE PS C:\> Get-MgaMailFolder Returns all folders in the mailbox of the connected user. .EXAMPLE PS C:\> Get-MgaMailFolder -Filter Inbox -User "max.master@contoso.onmicrosoft.com" -Token $Token Retrieves the inbox folder of the "max.master@contoso.onmicrosoft.com" mailbox, using the connection token stored in $Token. #> [CmdletBinding(DefaultParameterSetName = 'Default')] [OutputType([MSGraph.Exchange.Mail.Folder])] param ( [Parameter(ParameterSetName = 'ByFolderName', ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true, Position = 0)] [Alias('FolderName', 'InputObject', 'DisplayName', 'Id')] [MSGraph.Exchange.Mail.MailFolderParameter[]] $Name, [switch] $IncludeChildFolders, [switch] $Recurse, [string] $Filter = "*", [string] $User, [Int64] $ResultSize = (Get-PSFConfigValue -FullName 'MSGraph.Query.ResultSize' -Fallback 100), [MSGraph.Core.AzureAccessToken] $Token ) begin { if ($Recurse) { $IncludeChildFolders = $true } function invoke-internalMgaGetMethod ($invokeParam, [int]$level, [MSGraph.Exchange.Mail.Folder]$parentFolder) { $folderData = Invoke-MgaGetMethod @invokeParam | Where-Object displayName -Like $Filter foreach ($folderOutput in $folderData) { $hash = @{ Id = $folderOutput.Id DisplayName = $folderOutput.DisplayName ParentFolderId = $folderOutput.ParentFolderId ChildFolderCount = $folderOutput.ChildFolderCount UnreadItemCount = $folderOutput.UnreadItemCount TotalItemCount = $folderOutput.TotalItemCount User = $folderOutput.User HierarchyLevel = $level } if($parentFolder) { $hash.Add("ParentFolder", $parentFolder) } $folderOutputObject = New-Object -TypeName MSGraph.Exchange.Mail.Folder -Property $hash $folderOutputObject } } function get-childfolder ($output, $level, $invokeParam){ $FoldersWithChilds = $output | Where-Object ChildFolderCount -gt 0 $childFolders = @() do { $level = $level + 1 foreach ($folderItem in $FoldersWithChilds) { if($folderItem.ChildFolderCount -gt 0) { Write-PSFMessage -Level VeryVerbose -Message "Getting childfolders for folder '$($folderItem.Name)'" -Tag "ParameterSetHandling" $invokeParam.Field = "mailFolders/$($folderItem.Id)/childFolders" $childFolderOutput = invoke-internalMgaGetMethod -invokeParam $invokeParam -level $level -parentFolder $folderItem $FoldersWithChilds = $childFolderOutput | Where-Object ChildFolderCount -gt 0 $childFolders = $childFolders + $childFolderOutput } } } while ($Recurse -and $FoldersWithChilds) $childFolders } } process { Write-PSFMessage -Level VeryVerbose -Message "Gettings folder(s) by parameter set $($PSCmdlet.ParameterSetName)" -Tag "ParameterSetHandling" switch ($PSCmdlet.ParameterSetName) { "Default" { $level = 1 $invokeParam = @{ "Field" = 'mailFolders' "Token" = $Token "User" = Resolve-UserString -User $User "ResultSize" = $ResultSize "FunctionName" = $MyInvocation.MyCommand } $output = invoke-internalMgaGetMethod -invokeParam $invokeParam -level $level if ($output -and $IncludeChildFolders) { $childFolders = $output | Where-Object ChildFolderCount -gt 0 | ForEach-Object { get-childfolder -output $_ -level $level -invokeParam $invokeParam } if($childFolders) { [array]$output = [array]$output + $childFolders } } $output } "ByFolderName" { foreach ($folder in $Name) { $level = 1 Write-PSFMessage -Level VeryVerbose -Message "Getting folder '$( if($folder.Name){$folder.Name}else{$folder.Id} )'" -Tag "ParameterSetHandling" $invokeParam = @{ "Field" = "mailFolders/$($folder.Id)" "Token" = $Token "User" = Resolve-UserString -User $User "ResultSize" = $ResultSize "FunctionName" = $MyInvocation.MyCommand } $output = invoke-internalMgaGetMethod -invokeParam $invokeParam -level $level if ($output -and $IncludeChildFolders) { $childFolders = get-childfolder -output $output -level $level -invokeParam $invokeParam if($childFolders) { [array]$output = [array]$output + $childFolders } } $output } } Default { stop-PSFMessage -Message "Unhandled parameter set. ($($PSCmdlet.ParameterSetName)) Developer mistage." -EnableException $true -Category "ParameterSetHandling" -FunctionName $MyInvocation.MyCommand } } } end { } } function Get-MgaMailMessage { <# .SYNOPSIS Retrieves messages from a email folder from Exchange Online using the graph api. .DESCRIPTION Retrieves messages from a email folder from Exchange Online using the graph api. .PARAMETER InputObject Carrier object for Pipeline input Accepts messages or folders from other Mga-functions .PARAMETER Folder The display name of the folder to search. Defaults to the inbox. .PARAMETER User The user-account to access. Defaults to the main user connected as. Can be any primary email name of any user the connected token has access to. .PARAMETER Subject The subject to filter by (Client Side filtering) .PARAMETER Delta Indicates a "delta-query" for incremental changes on mails. The switch allows you to query mutliple times against the same user and folder while only getting additional, updated or deleted messages. Please notice, that delta queries needs to be handeled right. See the examples for correct usage. .PARAMETER ResultSize The user to execute this under. Defaults to the user the token belongs to. .PARAMETER Token The token representing an established connection to the Microsoft Graph Api. Can be created by using New-MgaAccessToken. Can be omitted if a connection has been registered using the -Register parameter on New-MgaAccessToken. .EXAMPLE PS C:\> Get-MgaMailMessage Return emails in the inbox of the user connected to through a token .EXAMPLE PS C:\> $mails = Get-MgaMailMessage -Delta Return emails in the inbox of the user connected to through a token and write the output in the variable $mails. IMPORTANT, the -Delta switch needs to be specified on the first call, because the outputobject will has to be piped into the next delta query. The content of $mails can be used and processed: PS C:\> $mails So the second Get-MgaMailMessage call has to be: PS C:\> $deltaMails = Get-MgaMailMessage -InputObject $mails -Delta This return only unqueried, updated, or new messages from the previous call and writes the result in the variable $deltaMails. The content of the $deltaMails variable can be used as output and should only overwrites the $mail variable if there is content in $deltaMails: PS C:\> if($deltaMails) { $mails = $deltaMails $deltaMails } From the second call, the procedure can be continued as needed, only updates will be outputted by Get-MgaMailMessage. .EXAMPLE PS C:\> Get-MgaMailFolder -Filter "MyFolder*" | Get-MgaMailMessage Return emails in the folders "MyFolder*" of the user connected to through a token .EXAMPLE PS C:\> Get-MgaMailMessage Return emails in the folders "MyFolder*" of the user connected to through a token #> [CmdletBinding(DefaultParameterSetName = 'ByFolderName')] [OutputType([MSGraph.Exchange.Mail.Message])] param ( [Parameter(ParameterSetName = 'ByInputObject', ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] $InputObject, [Parameter(ParameterSetName = 'ByFolderName', ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FolderName')] [string[]] $Folder = 'Inbox', [Parameter(ParameterSetName = 'ByFolderName')] [string] $User, [string] $Subject = "*", [switch] $Delta, [Int64] $ResultSize = (Get-PSFConfigValue -FullName 'MSGraph.Query.ResultSize' -Fallback 100), [MSGraph.Core.AzureAccessToken] $Token ) begin { $InvokeParams = @() } process { Write-PSFMessage -Level VeryVerbose -Message "Gettings mails by parameter set $($PSCmdlet.ParameterSetName)" -Tag "ParameterSetHandling" switch ($PSCmdlet.ParameterSetName) { "ByInputObject" { $typeNames = ($InputObject | Get-Member).TypeName | Sort-Object -Unique foreach($typeName in $typeNames) { switch ($typeName) { "MSGraph.Exchange.Mail.Message" { Write-PSFMessage -Level VeryVerbose -Message "Parsing messages from the pipeline" [array]$messages = $InputObject | Where-Object { "MSGraph.Exchange.Mail.Message" -in $_.psobject.TypeNames } if($Delta) { # retreive delta messages [array]$deltaMessages = $messages | Where-Object { '@odata.deltaLink' -in $_.BaseObject.psobject.Properties.Name } [array]$deltaLinks = $deltaMessages.BaseObject | Select-Object -ExpandProperty '@odata.deltaLink' -Unique Write-PSFMessage -Level VeryVerbose -Message "Delta parameter specified. Checking on $($deltaLinks.Count) deltalink(s) in $($deltaMessages.Count) message(s) from the pipeline" # build hashtable for Invoke-MgaGetMethod parameter splatting foreach($deltaLink in $deltaLinks) { $invokeParams = $invokeParams + @{ "deltaLink" = $deltaLink "Token" = $Token "ResultSize" = $ResultSize } } # filtering out delta-messages to get owing message in messages-array [array]$messages = $messages | Where-Object { $_.BaseObject.id -notin $deltaMessages.BaseObject.id } Remove-Variable deltaLinks, deltaMessages } # if non delta messages are parsed in, the messages will be queried again (refresh). Not really necessary, but intend from pipeline usage if($messages) { Write-PSFMessage -Level VeryVerbose -Message "Refresh message for $($messages.count) message(s) from the pipeline" foreach($message in $messages) { $invokeParam = @{ "Field" = "messages/$($message.id)" "User" = $message.BaseObject.User "Token" = $Token "ResultSize" = $ResultSize "FunctionName" = $MyInvocation.MyCommand } if($Delta) { $invokeParam.Add("Delta", $true) } $invokeParams = $invokeParams + $invokeParam } } Remove-Variable messages } "MSGraph.Exchange.Mail.Folder" { $folders = $InputObject | Where-Object { "MSGraph.Exchange.Mail.Folder" -in $_.psobject.TypeNames } foreach($folderItem in $folders) { Write-PSFMessage -Level VeryVerbose -Message "Gettings messages in folder '$($folderItem.Name)' from the pipeline" $invokeParam = @{ "Field" = "mailFolders/$($folderItem.Id)/messages" "User" = $folderItem.User "Token" = $Token "ResultSize" = $ResultSize "FunctionName" = $MyInvocation.MyCommand } if($Delta) { $invokeParam.Add("Delta", $true) } $invokeParams = $invokeParams + $invokeParam } Remove-Variable Folders } Default { Write-PSFMessage -Level Critical -Message "Failed on type validation. Can not handle $typeName" -EnableException $true -Tag "TypeValidation" } } } Remove-Variable typeNames } "ByFolderName" { foreach ($folderItem in $Folder) { Write-PSFMessage -Level VeryVerbose -Message "Getting messages in specified folder '$($folderItem.Name)'" # construct parameters for message query $invokeParam = @{ "Field" = "mailFolders/$($folderItem)/messages" "User" = $User "Token" = $Token "ResultSize" = $ResultSize "FunctionName" = $MyInvocation.MyCommand } if($Delta) { $invokeParam.Add("Delta", $true) } $InvokeParams = $InvokeParams + $InvokeParam } } Default { stop-PSFMessage -Message "Unhandled parameter set. ($($PSCmdlet.ParameterSetName)) Developer mistage." -EnableException $true -Category "ParameterSetHandling" -FunctionName $MyInvocation.MyCommand } } } end { $fielList = @() $InvokeParamsUniqueList = @() foreach($invokeParam in $InvokeParams) { if($invokeParam.Field -notin $fielList) { $InvokeParamsUniqueList = $InvokeParamsUniqueList + $invokeParam $fielList = $fielList + $invokeParam.Field } } Write-PSFMessage -Level Verbose -Message "Invoking $( ($InvokeParamsUniqueList | Measure-Object).Count ) REST calls for gettings messages" #-FunctionName $MyInvocation.MyCommand # run the message query and process the output foreach($invokeParam in $InvokeParamsUniqueList) { $data = Invoke-MgaGetMethod @invokeParam | Where-Object { $_.subject -like $Subject } foreach ($output in $data) { [MSGraph.Exchange.Mail.Message]@{ BaseObject = $output } } } } } function Move-MgaMailMessage { <# .SYNOPSIS Move message(s) to a folder .DESCRIPTION Move message(s) to a folder in Exchange Online using the graph api. .PARAMETER InputObject Carrier object for Pipeline input. Accepts messages. .PARAMETER Id The ID of the message to update .PARAMETER User The user-account to access. Defaults to the main user connected as. Can be any primary email name of any user the connected token has access to. .PARAMETER DestinationFolder The destination folder where to move the message to .PARAMETER Token The token representing an established connection to the Microsoft Graph Api. Can be created by using New-EORAccessToken. Can be omitted if a connection has been registered using the -Register parameter on New-EORAccessToken. .PARAMETER PassThru Outputs the token to the console .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> $mails | Move-MgaMailMessage -DestinationFolder $destinationFolder Moves messages in variable $mails to the folder in the variable $destinationFolder. The variable $mails can be represent: PS C:\> $mails = Get-MgaMailMessage -Folder Inbox -ResultSize 1 The variable $destinationFolder can be represent: PS C:\> $destinationFolder = Get-MgaMailFolder -Filter "Archive" .EXAMPLE PS C:\> Move-MgaMailMessage -Id $mails.id -DestinationFolder $destinationFolder Moves messages into the folder $destinationFolder. The variable $destinationFolder can be represent: PS C:\> $destinationFolder = Get-MgaMailFolder -Filter "Archive" .EXAMPLE PS C:\> Get-MgaMailMessage -Folder Inbox | Move-MgaMailMessage -DestinationFolder $destinationFolder Moves ALL messages from your inbox into the folder $destinationFolder. The variable $destinationFolder can be represent: PS C:\> $destinationFolder = Get-MgaMailFolder -Filter "Archive" #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium', DefaultParameterSetName = 'ByInputObject')] [Alias()] [OutputType([MSGraph.Exchange.Mail.Message])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByInputObject')] [Alias("Message")] [MSGraph.Exchange.Mail.Message] $InputObject, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ById')] [Alias("MessageId")] [string[]] $Id, [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ById')] [string] $User, [Parameter(Mandatory = $true)] [MSGraph.Exchange.Mail.Folder] $DestinationFolder, [MSGraph.Core.AzureAccessToken] $Token, [switch] $PassThru ) begin { } process { $messages = @() # Get input from pipeable objects Write-PSFMessage -Level Debug -Message "Gettings messages by parameter set $($PSCmdlet.ParameterSetName)" -Tag "ParameterSetHandling" switch ($PSCmdlet.ParameterSetName) { "ByInputObject" { $messages = $InputObject.Id $User = $InputObject.BaseObject.User } "ById" { $messages = $Id } Default { Stop-PSFFunction -Tag "ParameterSetHandling" -Message "Unhandled parameter set. ($($PSCmdlet.ParameterSetName)) Developer mistage." -EnableException $true -Exception ([System.Management.Automation.RuntimeException]::new("Unhandled parameter set. ($($PSCmdlet.ParameterSetName)) Developer mistage.")) -FunctionName $MyInvocation.MyCommand } } $bodyHash = @{ destinationId = ($DestinationFolder.Id | ConvertTo-Json) } #region Put parameters (JSON Parts) into a valid "message"-JSON-object together $bodyJsonParts = @() foreach ($key in $bodyHash.Keys) { $bodyJsonParts = $bodyJsonParts + """$($key)"" : $($bodyHash[$Key])" } $bodyJSON = "{`n" + ([string]::Join(",`n", $bodyJsonParts)) + "`n}" #endregion Put parameters (JSON Parts) into a valid "message"-JSON-object together #region move messages foreach ($messageId in $messages) { if ($pscmdlet.ShouldProcess("messageId $($messageId)", "Move to folder '$($DestinationFolder.Name)'")) { Write-PSFMessage -Tag "MessageUpdate" -Level Verbose -Message "Move messageId '$($messageId)' to folder '$($DestinationFolder.Name)'" $invokeParam = @{ "Field" = "messages/$($messageId)/move" "User" = $User "Body" = $bodyJSON "ContentType" = "application/json" "Token" = $Token "FunctionName" = $MyInvocation.MyCommand } $output = Invoke-MgaPostMethod @invokeParam if ($PassThru) { [MSGraph.Exchange.Mail.Message]@{ BaseObject = $output } } } } #endregion Update messages } } function Set-MgaMailMessage { <# .SYNOPSIS Set properties on message(s) .DESCRIPTION Set properties on message(s) in Exchange Online using the graph api. .PARAMETER InputObject Carrier object for Pipeline input. Accepts messages. .PARAMETER Id The ID of the message to update .PARAMETER User The user-account to access. Defaults to the main user connected as. Can be any primary email name of any user the connected token has access to. .PARAMETER IsRead Indicates whether the message has been read. .PARAMETER Subject The subject of the message. (Updatable only if isDraft = true.) .PARAMETER Sender The account that is actually used to generate the message. (Updatable only if isDraft = true, and when sending a message from a shared mailbox, or sending a message as a delegate. In any case, the value must correspond to the actual mailbox used.) .PARAMETER From The mailbox owner and sender of the message. Must correspond to the actual mailbox used. (Updatable only if isDraft = true.) .PARAMETER ToRecipients The To recipients for the message. (Updatable only if isDraft = true.) .PARAMETER CCRecipients The Cc recipients for the message. (Updatable only if isDraft = true.) .PARAMETER BCCRecipients The Bcc recipients for the message. (Updatable only if isDraft = true.) .PARAMETER ReplyTo The email addresses to use when replying. (Updatable only if isDraft = true.) .PARAMETER Body The body of the message. (Updatable only if isDraft = true.) .PARAMETER Categories The categories associated with the message. .PARAMETER Importance The importance of the message. The possible values are: Low, Normal, High. .PARAMETER InferenceClassification The classification of the message for the user, based on inferred relevance or importance, or on an explicit override. The possible values are: focused or other. .PARAMETER InternetMessageId The message ID in the format specified by RFC2822. (Updatable only if isDraft = true.) .PARAMETER IsDeliveryReceiptRequested Indicates whether a delivery receipt is requested for the message. .PARAMETER IsReadReceiptRequested Indicates whether a read receipt is requested for the message. .PARAMETER Token The token representing an established connection to the Microsoft Graph Api. Can be created by using New-EORAccessToken. Can be omitted if a connection has been registered using the -Register parameter on New-EORAccessToken. .PARAMETER PassThru Outputs the token to the console .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> $mail | Set-MgaMailMessage -IsRead $false Set messages represented by variable $mail to status "unread" The variable $mails can be represent: PS C:\> $mails = Get-MgaMailMessage -Folder Inbox -ResultSize 1 .EXAMPLE PS C:\> $mail | Set-MgaMailMessage -IsRead $false -categories "Red category" Set status "unread" and category "Red category" to messages represented by variable $mail The variable $mails can be represent: PS C:\> $mails = Get-MgaMailMessage -Folder Inbox -ResultSize 1 .EXAMPLE PS C:\> $mail | Set-MgaMailMessage -ToRecipients "someone@something.org" Set reciepent from draft mail represented by variable $mail The variable $mails can be represent: PS C:\> $mails = Get-MgaMailMessage -Folder Drafts .EXAMPLE PS C:\> Set-MgaMailMessage -Id $mail.Id -ToRecipients "someone@something.org" -Subject "Something important" Set reciepent from draft mail represented by variable $mail The variable $mails can be represent: PS C:\> $mails = Get-MgaMailMessage -Folder Drafts .EXAMPLE PS C:\> $mail | Set-MgaMailMessage -ToRecipients $null Clear reciepent from draft mail represented by variable $mail The variable $mails can be represent: PS C:\> $mails = Get-MgaMailMessage -Folder Drafts #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium', DefaultParameterSetName = 'ByInputObject')] [Alias("Update-MgaMailMessage")] [OutputType([MSGraph.Exchange.Mail.Message])] param ( [Parameter(Mandatory=$true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByInputObject')] [Alias("Message")] [MSGraph.Exchange.Mail.Message] $InputObject, [Parameter(Mandatory=$true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ById')] [Alias("MessageId")] [string[]] $Id, [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ById')] [string] $User, [ValidateNotNullOrEmpty()] [bool] $IsRead, [string] $Subject, [AllowNull()] [AllowEmptyCollection()] [AllowEmptyString()] [string] $Sender, [AllowNull()] [AllowEmptyCollection()] [AllowEmptyString()] [string] $From, [AllowNull()] [AllowEmptyCollection()] [AllowEmptyString()] [string[]] $ToRecipients, [AllowNull()] [AllowEmptyCollection()] [AllowEmptyString()] [string[]] $CCRecipients, [AllowNull()] [AllowEmptyCollection()] [AllowEmptyString()] [string[]] $BCCRecipients, [AllowNull()] [AllowEmptyCollection()] [AllowEmptyString()] [string[]] $ReplyTo, [String] $Body, [String[]] $Categories, [ValidateSet("Low", "Normal", "High")] [String] $Importance, [ValidateSet("focused", "other")] [String] $InferenceClassification, [String] $InternetMessageId, [bool] $IsDeliveryReceiptRequested, [bool] $IsReadReceiptRequested, [MSGraph.Core.AzureAccessToken] $Token, [switch] $PassThru ) begin { $boundParameters = @() $mailAddressNames = @("sender", "from", "toRecipients", "ccRecipients", "bccRecipients", "replyTo") # parsing mailAddress parameter strings to mailaddress objects (if not empty) foreach ($Name in $mailAddressNames) { if (Test-PSFParameterBinding -ParameterName $name) { New-Variable -Name "$($name)Addresses" -Force -Scope 0 if( (Get-Variable -Name $Name -Scope 0).Value ) { try { Set-Variable -Name "$($name)Addresses" -Value ( (Get-Variable -Name $Name -Scope 0).Value | ForEach-Object { [mailaddress]$_ } -ErrorAction Stop -ErrorVariable parseError ) } catch { Stop-PSFFunction -Message "Unable to parse $($name) to a mailaddress. String should be 'name@domain.topleveldomain' or 'displayname name@domain.topleveldomain'. Error: $($parseError[0].Exception)" -Tag "ParameterParsing" -Category InvalidData -EnableException $true -Exception $parseError[0].Exception } } } } } process { $messages = @() $bodyHash = @{} # Get input from pipeable objects Write-PSFMessage -Level Debug -Message "Gettings messages by parameter set $($PSCmdlet.ParameterSetName)" -Tag "ParameterSetHandling" switch ($PSCmdlet.ParameterSetName) { "ByInputObject" { $messages = $InputObject.Id $User = $InputObject.BaseObject.User } "ById" { $messages = $Id } Default { Stop-PSFFunction -Tag "ParameterSetHandling" -Message "Unhandled parameter set. ($($PSCmdlet.ParameterSetName)) Developer mistage." -EnableException $true -Exception ([System.Management.Automation.RuntimeException]::new("Unhandled parameter set. ($($PSCmdlet.ParameterSetName)) Developer mistage.")) -FunctionName $MyInvocation.MyCommand } } #region Parsing string and boolean parameters to json data parts $names = @("IsRead", "Subject", "Body", "Categories", "Importance", "InferenceClassification", "InternetMessageId", "IsDeliveryReceiptRequested", "IsReadReceiptRequested") Write-PSFMessage -Level VeryVerbose -Message "Parsing string and boolean parameters to json data parts ($([string]::Join(", ", $names)))" -Tag "ParameterParsing" foreach ( $name in $names ) { if (Test-PSFParameterBinding -ParameterName $name) { $boundParameters = $boundParameters + $name Write-PSFMessage -Level Debug -Message "Parsing text parameter $($name)" -Tag "ParameterParsing" $bodyHash.Add($name, ((Get-Variable $name -Scope 0).Value| ConvertTo-Json)) } } #endregion Parsing string and boolean parameters to json data parts #region Parsing mailaddress parameters to json data parts Write-PSFMessage -Level VeryVerbose -Message "Parsing mailaddress parameters to json data parts ($([string]::Join(", ", $mailAddressNames)))" -Tag "ParameterParsing" foreach ( $name in $mailAddressNames ) { if (Test-PSFParameterBinding -ParameterName $name) { $boundParameters = $boundParameters + $name Write-PSFMessage -Level Debug -Message "Parsing mailaddress parameter $($name)" -Tag "ParameterParsing" $addresses = (Get-Variable -Name "$($name)Addresses" -Scope 0).Value if ($addresses) { # build valid mail address object, if address is specified [array]$addresses = foreach ($item in $addresses) { [PSCustomObject]@{ emailAddress = [PSCustomObject]@{ address = $item.Address name = $item.DisplayName } } } } else { # place an empty mail address object in, if no address is specified (this will clear the field in the message) [array]$addresses = [PSCustomObject]@{ emailAddress = [PSCustomObject]@{ address = "" name = "" } } } if ($name -in @("toRecipients", "ccRecipients", "bccRecipients", "replyTo")) { # these kind of objects need to be an JSON array if ($addresses.Count -eq 1) { # hardly format JSON object as an array, because ConvertTo-JSON will output a single object-json-string on an array with count 1 (PSVersion 5.1.17134.407 | PSVersion 6.1.1) $bodyHash.Add($name, ("[" + ($addresses | ConvertTo-Json) + "]") ) } else { $bodyHash.Add($name, ($addresses | ConvertTo-Json) ) } } else { $bodyHash.Add($name, ($addresses | ConvertTo-Json) ) } } } #endregion Parsing mailaddress parameters to json data parts #region Put parameters (JSON Parts) into a valid "message"-JSON-object together $bodyJsonParts = @() foreach ($key in $bodyHash.Keys) { $bodyJsonParts = $bodyJsonParts + """$($key)"" : $($bodyHash[$Key])" } $bodyJSON = "{`n" + ([string]::Join(",`n", $bodyJsonParts)) + "`n}" #endregion Put parameters (JSON Parts) into a valid "message"-JSON-object together #region Update messages foreach ($messageId in $messages) { if ($pscmdlet.ShouldProcess("messageId $($messageId)", "Update properties '$([string]::Join("', '", $boundParameters))'")) { Write-PSFMessage -Level Verbose -Message "Update properties '$([string]::Join("', '", $boundParameters))' on messageId $($messageId)" -Tag "MessageUpdate" $invokeParam = @{ "Field" = "messages/$($messageId)" "User" = $User "Body" = $bodyJSON "ContentType" = "application/json" "Token" = $Token "FunctionName" = $MyInvocation.MyCommand } $output = Invoke-MgaPatchMethod @invokeParam if ($PassThru) { [MSGraph.Exchange.Mail.Message]@{ BaseObject = $output } } } } #endregion Update messages } } <# This is an example configuration file By default, it is enough to have a single one of them, however if you have enough configuration settings to justify having multiple copies of it, feel totally free to split them into multiple files. #> <# # Example Configuration Set-PSFConfig -Module 'MSGraph' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'" #> Set-PSFConfig -Module 'MSGraph' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging." Set-PSFConfig -Module 'MSGraph' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." Set-PSFConfig -Module 'MSGraph' -Name 'Tenant.Application.ClientID' -Value "1930085c-c139-42f5-8d1a-0b9c88ca43e3" -Initialize -Validation 'string' -Description "Well known ClientID from registered Application in Azure tenant" Set-PSFConfig -Module 'MSGraph' -Name 'Tenant.Application.RedirectUrl' -Value "https://localhost" -Initialize -Validation 'string' -Description "Redirection URL specified in MS Azure Application portal for the registered application" Set-PSFConfig -Module 'MSGraph' -Name 'Query.ResultSize' -Value 100 -Initialize -Validation integer -Description "Limit of amount of records returned by a function. Use 0 for unlimited." Set-PSFConfig -Module 'MSGraph' -Name 'Hierarchy.Path.Separator' -Value "\" -Initialize -Validation string -Description "the character used to process hierarchical names (like FullName property on folders) in MSGraph module." <# # Example: Register-PSFTeppScriptblock -Name "MSGraph.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' } #> Register-PSFTeppScriptblock -Name "MSGraph.Exchange.Mail.WellKnowFolders" -ScriptBlock { [enum]::GetNames([MSGraph.Exchange.Mail.WellKnownFolder]) | ForEach-Object { (Get-Culture).TextInfo.ToTitleCase( $_ ) } } <# # Example: Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name MSGraph.alcohol #> Register-PSFTeppArgumentCompleter -Command Get-MgaMailFolder -Parameter "Name" -Name "MSGraph.Exchange.Mail.WellKnowFolders" New-PSFLicense -Product 'MSGraph' -Manufacturer 'Friedrich Weinmann' -ProductVersion $ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2018-08-28") -Text @" Copyright (c) 2018 Friedrich Weinmann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. "@ #endregion Load compiled code |