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]) ) # Work on payload $tokenPayload = [System.Text.Encoding]::UTF8.GetString( (ConvertFrom-Base64StringWithNoPadding $tokenParts[1]) ) # Work on signature $tokenSignature = ConvertFrom-Base64StringWithNoPadding $tokenParts[2] # Output $resultObject = New-Object MSGraph.Core.JWTAccessTokenInfo -Property @{ Header = $tokenHeader Payload = $tokenPayload Signature = $tokenSignature } #$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-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') { 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 ResultSize The user to execute this under. Defaults to the user the token belongs to. .PARAMETER Token The access token to use to connect. .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()] param ( [string[]] $Field, [string] $User = "me", [Int64] $ResultSize = (Get-PSFConfigValue -FullName 'MSGraph.Query.ResultSize' -Fallback 100), $Token ) 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 -Cmdlet $PSCmdlet } 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 } if ($ResultSize -eq 0) { $ResultSize = [Int64]::MaxValue } [Int64]$i = 0 $restLink = "https://graph.microsoft.com/v1.0/$(Resolve-UserString -User $User)/$($Field)" do { $data = Invoke-RestMethod -Method Get -UseBasicParsing -Uri $restLink -Headers @{ "Authorization" = "Bearer $( [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($token.AccessToken)) )" "Prefer" = "outlook.timezone=`"$((Get-Timezone).Id)`"" } $data.Value $i = $i + $data.Value.Count if($i -lt $ResultSize ) { $restLink = $data.'@odata.nextLink' } else { $restLink = "" Write-PSFMessage -Level Warning -Message "Too many items. Reaching maximum ResultSize before finishing query. You may want to increase the ResultSize. Current ResultSize: $($ResultSize)" -Tag "GetData" -FunctionName $PSCmdlet } } while ($restLink) } 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 ( [MSGraph.Core.AzureAccessToken] $Token, [Parameter(ParameterSetName='Register')] [switch] $Register, [Parameter(ParameterSetName='Register')] [switch] $PassThru ) 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 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 = $Credential ClientId = $ClientId Resource = $resourceUri AppRedirectUrl = $Token.AppRedirectUrl } # 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 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()] $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) { [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()] 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')] #[PSCustomObject] #$InputObject, $objectBaseType = "MSGraph.Exchange" $objectType = "MailAttachment" } 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) { $output.pstypenames.Insert(0, $objectBaseType) $output.pstypenames.Insert(0, "$($objectBaseType).$($objectType)") $output } } } end { } } function Get-MgaMailFolder { <# .SYNOPSIS Searches mail folders in Exchange Online .DESCRIPTION Searches mail folders in Exchange Online .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.mustermann@contoso.onmicrosoft.com" -Token $Token Retrieves the inbox folder of the "max.mustermann@contoso.onmicrosoft.com" mailbox, using the connection token stored in $Token. #> [CmdletBinding()] param ( [string] $Filter = "*", [string] $User = 'me', [Int64] $ResultSize = (Get-PSFConfigValue -FullName 'MSGraph.Query.ResultSize' -Fallback 100), [MSGraph.Core.AzureAccessToken] $Token ) $objectBaseType = "MSGraph.Exchange" $objectType = "MailFolder" $data = Invoke-MgaGetMethod -Field 'mailFolders' -Token $Token -User (Resolve-UserString -User $User) -ResultSize $ResultSize | Where-Object displayName -Like $Filter foreach ($output in $data) { $output.pstypenames.Insert(0, $objectBaseType) $output.pstypenames.Insert(0, "$($objectBaseType).$($objectType)") $output } } 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 Name 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 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()] param ( [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName='ByName')] [Alias('DisplayName', 'FolderName')] [string[]] $Name = 'Inbox', [string] $User = 'me', [Int64] $ResultSize = (Get-PSFConfigValue -FullName 'MSGraph.Query.ResultSize' -Fallback 100), [MSGraph.Core.AzureAccessToken] $Token ) begin { #[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName='ByInputObject')] #[MSGraph.Exchange.MailFolder] #$InputObject, $objectBaseType = "MSGraph.Exchange" $objectType = "MailMessage" } process { foreach ($folder in $Name) { Write-PSFMessage -Level Verbose -Message "Searching $folder" #$data = Invoke-MgaGetMethod -Field "mailFolders('$($folder)')/messages" -User $User -Token $Token $data = Invoke-MgaGetMethod -Field "mailFolders/$($folder)/messages" -User $User -Token $Token -ResultSize $ResultSize foreach ($output in $data) { $output.pstypenames.Insert(0, $objectBaseType) $output.pstypenames.Insert(0, "$($objectBaseType).$($objectType)") $output } } } } <# 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." <# # Example: Register-PSFTeppScriptblock -Name "MSGraph.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' } #> <# # Example: Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name MSGraph.alcohol #> 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 |