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