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-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
            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"
        }
    }

    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 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()]
    [OutputType([MSGraph.Exchange.Mail.Folder])]
    param (
        [string]
        $Filter = "*",

        [string]
        $User,

        [Int64]
        $ResultSize = (Get-PSFConfigValue -FullName 'MSGraph.Query.ResultSize' -Fallback 100),

        [MSGraph.Core.AzureAccessToken]
        $Token
    )

    $invokeParam = @{
        "Field"        = 'mailFolders'
        "Token"        = $Token
        "User"         = Resolve-UserString -User $User
        "ResultSize"   = $ResultSize
        "FunctionName" = $MyInvocation.MyCommand
    }

    #Write-PSFMessage -Level Verbose -Message "--- $($invokeParam.Keys) | $($invokeParam.Values) ---" -FunctionName $MyInvocation.MyCommand
    $folderData = Invoke-MgaGetMethod @invokeParam | Where-Object displayName -Like $Filter
    foreach ($folderOutput in $folderData) {
        [MSGraph.Exchange.Mail.Folder]@{
            BaseObject = $folderOutput
        }
    }
}

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 $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
"<compile code into here>"
#endregion Load compiled code.psobject.TypeNames }

                            if($Delta) {
                                # retreive delta messages
                                [array]$deltaMessages = $messages | Where-Object { '@odata.deltaLink' -in $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
"<compile code into here>"
#endregion Load compiled code.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 { $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
"<compile code into here>"
#endregion Load compiled code.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 $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
"<compile code into here>"
#endregion Load compiled code.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.BaseObject.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 { $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
"<compile code into here>"
#endregion Load compiled code.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:\> Update-MgaMailMessage
 
        Update emails
    #>

    [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:\> Update-MgaMailMessage
 
        Update emails
    #>

    [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]$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
"<compile code into here>"
#endregion Load compiled code } -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."

<#
# 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