MSGraph.psm1

$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\MSGraph.psd1").ModuleVersion

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName MSGraph.Import.DoDotSource -Fallback $false
if ($MSGraph_dotsourcemodule) { $script:doDotSource = $true }

<#
Note on Resolve-Path:
All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator.
This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS.
Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist.
This is important when testing for paths.
#>


# Detect whether at some level loading individual module files, rather than the compiled module was enforced
$importIndividualFiles = Get-PSFConfigValue -FullName MSGraph.Import.IndividualFiles -Fallback $false
if ($MSGraph_importIndividualFiles) { $importIndividualFiles = $true }
if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }
    
function Import-ModuleFile
{
    <#
        .SYNOPSIS
            Loads files into the module on module import.
         
        .DESCRIPTION
            This helper function is used during module initialization.
            It should always be dotsourced itself, in order to proper function.
             
            This provides a central location to react to files being imported, if later desired
         
        .PARAMETER Path
            The path to the file to load
         
        .EXAMPLE
            PS C:\> . Import-ModuleFile -File $function.FullName
     
            Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Path
    )
    
    if ($doDotSource) { . (Resolve-Path $Path) }
    else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText((Resolve-Path $Path)))), $null, $null) }
}

#region Load individual files
if ($importIndividualFiles)
{
    # Execute Preimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1"
    
    # Import all internal functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Import all public functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Execute Postimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1"
    
    # End it here, do not load compiled code below
    return
}
#endregion Load individual files

#region Load compiled code
Add-Type -AssemblyName System.Net.Http
Add-Type -AssemblyName System.Web
Add-Type -AssemblyName System.Windows.Forms

function Convert-UriQueryFromHash {
    <#
    .SYNOPSIS
        Converts hashtables to a string for REST api calls.
 
    .DESCRIPTION
        Converts hashtables to a string for REST api calls.
 
    .PARAMETER hash
        The hashtable to convert to a string
 
    .PARAMETER NoQuestionmark
        Supress the ? as the first character in the output string
 
    .EXAMPLE
        PS C:\> Convert-UriQueryFromHash -Hash @{ username = "user"; password = "password"}
 
        Converts the specified hashtable to the following string:
        ?password=password&username=user
 
    .EXAMPLE
        PS C:\> Convert-UriQueryFromHash -Hash @{ username = "user"; password = "password"} -NoQuestionmark
 
        Converts the specified hashtable to the following string:
        password=password&username=user
        #>

    [OutputType([System.String])]
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [System.Collections.Hashtable]
        $Hash,

        [switch]
        $NoQuestionmark
    )

    begin {
    }

    process {
        $elements = foreach ($key in $Hash.Keys) {
            $key + "=" + $Hash[$key]
        }
        $elementString = [string]::Join("&", $elements)

        if($NoQuestionMark) {
            "$elementString"
        }
        else {
            "?$elementString"
        }
    }

    end {
    }
}

function ConvertFrom-Base64StringWithNoPadding( [string]$Data ) {
    <#
    .SYNOPSIS
        Helper function build valid Base64 strings from JWT access tokens
 
    .DESCRIPTION
        Helper function build valid Base64 strings from JWT access tokens
 
    .PARAMETER Data
        The Token to convert
 
    .EXAMPLE
        PS C:\> ConvertFrom-Base64StringWithNoPadding -Data $data
 
        build valid base64 string the content from variable $data
    #>

    $Data = $Data.Replace('-', '+').Replace('_', '/')
    switch ($Data.Length % 4) {
        0 { break }
        2 { $Data += '==' }
        3 { $Data += '=' }
        default { throw New-Object ArgumentException('data') }
    }
    [System.Convert]::FromBase64String($Data)
}

function ConvertFrom-JWTtoken {
    <#
    .SYNOPSIS
        Converts access tokens to readable objects
 
    .DESCRIPTION
        Converts access tokens to readable objects
 
    .PARAMETER Token
        The Token to convert
 
    .EXAMPLE
        PS C:\> ConvertFrom-JWTtoken -Token $Token
 
        Converts the content from variable $token to an object
    #>

    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Token
    )

    # Validate as per https://tools.ietf.org/html/rfc7519 - Access and ID tokens are fine, Refresh tokens will not work
    if ((-not $Token.Contains(".")) -or (-not $Token.StartsWith("eyJ"))) {
        Stop-PSFFunction -Message "Invalid data or not an access token" -EnableException -Tag JWT
    }

    # Split the token in its parts
    $tokenParts = $Token.Split(".")

    # Work on header
    $tokenHeader = [System.Text.Encoding]::UTF8.GetString( (ConvertFrom-Base64StringWithNoPadding $tokenParts[0]) )
    $tokenHeaderJSON = $tokenHeader | ConvertFrom-Json

    # Work on payload
    $tokenPayload = [System.Text.Encoding]::UTF8.GetString( (ConvertFrom-Base64StringWithNoPadding $tokenParts[1]) )
    $tokenPayloadJSON = $tokenPayload | ConvertFrom-Json

    # Work on signature
    $tokenSignature = ConvertFrom-Base64StringWithNoPadding $tokenParts[2]

    # Output
    $resultObject = New-Object MSGraph.Core.JWTAccessTokenInfo -Property @{
        Header               = $tokenHeader
        Payload              = $tokenPayload
        Signature            = $tokenSignature
        Algorithm            = $tokenHeaderJSON.alg
        Type                 = $tokenHeaderJSON.typ
        ApplicationID        = $tokenPayloadJSON.appid
        ApplicationName      = $tokenPayloadJSON.app_displayname
        Audience             = $tokenPayloadJSON.aud
        AuthenticationMethod = $tokenPayloadJSON.amr
        ExpirationTime       = ([datetime]"1970-01-01Z00:00:00").AddSeconds($tokenPayloadJSON.exp).ToUniversalTime()
        GivenName            = $tokenPayloadJSON.given_name
        IssuedAt             = ([datetime]"1970-01-01Z00:00:00").AddSeconds($tokenPayloadJSON.iat).ToUniversalTime()
        Name                 = $tokenPayloadJSON.name
        NotBefore            = ([datetime]"1970-01-01Z00:00:00").AddSeconds($tokenPayloadJSON.nbf).ToUniversalTime()
        OID                  = $tokenPayloadJSON.oid
        Plattform            = $tokenPayloadJSON.platf
        Scope                = $tokenPayloadJSON.scp
        SID                  = $tokenPayloadJSON.onprem_sid
        SourceIPAddr         = $tokenPayloadJSON.ipaddr
        SureName             = $tokenPayloadJSON.family_name
        TenantID             = $tokenPayloadJSON.tid
        UniqueName           = $tokenPayloadJSON.unique_name
        UPN                  = $tokenPayloadJSON.upn
        Version              = $tokenPayloadJSON.ver
    }

    #$output
    $resultObject
}

function New-HttpClient
{
    <#
    .SYNOPSIS
        Generates a HTTP Client for use with the Exchange Online Rest Api.
 
    .DESCRIPTION
        Generates a HTTP Client for use with the Exchange Online Rest Api.
 
    .PARAMETER MailboxName
        The mailbox to connect with.
 
    .EXAMPLE
        PS C:\> New-HttpClient -MailboxName 'foo@contoso.onmicrosoft.com'
 
        Creates a Http Client for connecting as 'foo@contoso.onmicrosoft.com'
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param ()
    process
    {
        $handler = New-Object System.Net.Http.HttpClientHandler
        $handler.CookieContainer = New-Object System.Net.CookieContainer
        $handler.AllowAutoRedirect = $true
        $httpClient = New-Object System.Net.Http.HttpClient($handler)

        $header = New-Object System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")
        $httpClient.DefaultRequestHeaders.Accept.Add($header)
        $httpClient.Timeout = New-Object System.TimeSpan(0, 0, 90)
        $httpClient.DefaultRequestHeaders.TransferEncodingChunked = $false
        $header = New-Object System.Net.Http.Headers.ProductInfoHeaderValue("RestClient", "1.1")
        $httpClient.DefaultRequestHeaders.UserAgent.Add($header)

        return $httpClient
    }
}

function New-MgaMailMessageObject {
    <#
    .SYNOPSIS
        Create new MessageObject
 
    .DESCRIPTION
        Create new MessageObject
        Helper function used for internal commands.
 
    .PARAMETER RestData
        The RestData object containing the data for the new message object.
 
    .PARAMETER FunctionName
        Name of the higher function which is calling this function.
 
    .EXAMPLE
        PS C:\> New-MgaMailMessageObject -RestData $output
 
        Create a MSGraph.Exchange.Mail.Message object from data in variable $output
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [OutputType([MSGraph.Exchange.Mail.Message])]
    [CmdletBinding()]
    param (
        $RestData,

        [String]
        $FunctionName
    )

    $hash = [ordered]@{
        BaseObject                 = $output
        Subject                    = $output.subject
        Body                       = $output.body
        BodyPreview                = $output.bodyPreview
        Categories                 = $output.categories
        ChangeKey                  = $output.changeKey
        ConversationId             = $output.conversationId
        CreatedDateTime            = [datetime]::Parse($output.createdDateTime)
        Flag                       = $output.flag.flagStatus
        HasAttachments             = $output.hasAttachments
        Id                         = $output.id
        Importance                 = $output.importance
        InferenceClassification    = $output.inferenceClassification
        InternetMessageId          = $output.internetMessageId
        IsDeliveryReceiptRequested = $output.isDeliveryReceiptRequested
        IsDraft                    = $output.isDraft
        IsRead                     = $output.isRead
        isReadReceiptRequested     = $output.isReadReceiptRequested
        lastModifiedDateTime       = [datetime]::Parse($output.lastModifiedDateTime)
        MeetingMessageType         = $output.meetingMessageType
        ParentFolderId             = $output.parentFolderId
        ReceivedDateTime           = [datetime]::Parse($output.receivedDateTime)
        SentDateTime               = [datetime]::Parse($output.sentDateTime)
        WebLink                    = $output.webLink
    }
    if($output.from.emailAddress) {
        $hash.Add("from", ($output.from.emailAddress | ForEach-Object { [mailaddress]"$($_.name) $($_.address)"} -ErrorAction Continue))
    }
    if($output.Sender.emailAddress) {
        $hash.Add("Sender", ($output.Sender.emailAddress | ForEach-Object { [mailaddress]"$($_.name) $($_.address)"} -ErrorAction Continue ))
    }
    if($output.bccRecipients.emailAddress) {
        $hash.Add("bccRecipients", [array]($output.bccRecipients.emailAddress | ForEach-Object { [mailaddress]"$($_.name) $($_.address)"} -ErrorAction Continue))
    }
    if($output.ccRecipients.emailAddress) {
        $hash.Add("ccRecipients", [array]($output.ccRecipients.emailAddress | ForEach-Object { [mailaddress]"$($_.name) $($_.address)"} -ErrorAction Continue))
    }
    if($output.replyTo.emailAddress) {
        $hash.Add("replyTo", [array]($output.replyTo.emailAddress | ForEach-Object { [mailaddress]"$($_.name) $($_.address)"} -ErrorAction Continue))
    }
    if($output.toRecipients.emailAddress) {
        $hash.Add("toRecipients", [array]($output.toRecipients.emailAddress | ForEach-Object { [mailaddress]"$($_.name) $($_.address)"}))
    }

    $messageOutputObject = New-Object -TypeName MSGraph.Exchange.Mail.Message -Property $hash
    $messageOutputObject
}

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 (
        [ValidateNotNullOrEmpty()]
        [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 (
        [Parameter(Mandatory = $true)]
        [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 (
        [Parameter(Mandatory = $true)]
        [string]
        $Field,

        [string]
        $User,

        [String]
        $Body,

        [ValidateSet("application/json")]
        [String]
        $ContentType = "application/json",

        [MSGraph.Core.AzureAccessToken]
        $Token,

        [string]
        $FunctionName = $MyInvocation.MyCommand
    )

    $Token = Resolve-Token -Token $Token -FunctionName $FunctionName
    if (-not $User) { $User = $Token.UserprincipalName }
    $restUri = "https://graph.microsoft.com/v1.0/$(Resolve-UserString -User $User)/$($Field)"

    Write-PSFMessage -Tag "RestData" -Level VeryVerbose -Message "Invoking REST POST to uri: $($restUri)"
    Write-PSFMessage -Tag "RestData" -Level Debug -Message "REST body data: $($Body)"

    Clear-Variable -Name data -Force -WhatIf:$false -Confirm:$false -Verbose:$false -ErrorAction Ignore
    $invokeParam = @{
        Method          = "Post"
        Uri             = $restUri
        Body            = $Body
        Headers         = @{
            "Authorization" = "Bearer $( [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($token.AccessToken)) )"
            "Content-Type"  = "application/json"
        }
    }
    $data = Invoke-RestMethod @invokeParam -ErrorVariable "restError" -Verbose:$false -UseBasicParsing

    if ($restError) {
        Stop-PSFFunction -Tag "RestData" -Message $parseError[0].Exception -Exception $parseError[0].Exception -EnableException $false -Category ConnectionError -FunctionName $FunctionName
        return
    }

    $data | Add-Member -MemberType NoteProperty -Name 'User' -Value $User -Force
    $data
}

function New-MgaAccessToken {
    <#
    .SYNOPSIS
        Creates an access token for contacting the specified application endpoint
 
    .DESCRIPTION
        Creates an access token for contacting the specified application endpoint
 
    .PARAMETER MailboxName
        The email address of the mailbox to access
 
    .PARAMETER Credential
        The credentials to use to authenticate the request.
        Using this avoids the need to visually interact with the logon screen.
        Only works for accounts that have once logged in visually, but can be used from any machine.
 
    .PARAMETER ClientId
        The ID of the client to connect with.
        This is the ID of the registered application.
 
    .PARAMETER RedirectUrl
        Some weird vodoo. Leave it as it is, unless you know better
 
    .PARAMETER Refresh
        Try to do a refresh login dialag, which may possibly avoid entering password again.
 
    .PARAMETER Register
        Registers the token, so all subsequent calls to Exchange Online reuse it by default.
 
    .PARAMETER PassThru
        Outputs the token to the console, even when the register switch is set
 
    .EXAMPLE
        PS C:\> New-MgaAccessToken -MailboxName 'max.musterman@contoso.com'
 
        Registers an application to run under 'max.mustermann@contoso.com'.
        Requires an interactive session with a user handling the web UI.
 
    .EXAMPLE
        PS C:\> New-MgaAccessToken -MailboxName 'max.musterman@contoso.com' -Credential $cred
 
        Generates a token to a session as max.mustermann@contoso.com under the credentials specified in $cred.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(DefaultParameterSetName="Default")]
    param (
        [PSCredential]
        $Credential,

        [System.Guid]
        $ClientId = (Get-PSFConfigValue -FullName MSGraph.Tenant.Application.ClientID -NotNull),

        [string]
        $RedirectUrl = (Get-PSFConfigValue -FullName MSGraph.Tenant.Application.RedirectUrl -Fallback "urn:ietf:wg:oauth:2.0:oob"),

        [switch]
        $Refresh,

        [Parameter(ParameterSetName='Register')]
        [switch]
        $Register,

        [Parameter(ParameterSetName='Register')]
        [switch]
        $PassThru
    )

    # variable definitions
    $resourceUri = "https://graph.microsoft.com"
    $baselineTimestamp = [datetime]"1970-01-01Z00:00:00"
    $endpointUri = "https://login.windows.net/common/oauth2"
    $endpointUriAuthorize = "$($endpointUri)/authorize"
    $endpointUriToken = "$($endpointUri)/token "

    # Creating http client for logon
    $httpClient = New-HttpClient

    if (-not $Credential) {
        # Request an authorization code with web form
        # Info https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-protocols-oauth-code#request-an-authorization-code
        Write-PSFMessage -Level Verbose -Message "Authentication is done by code. Query authentication from login form." -Tag "Authorization"

        $queryHash = [ordered]@{
            resource      = [System.Web.HttpUtility]::UrlEncode($resourceUri)
            client_id     = "$($ClientId)"
            response_type = "code"
            redirect_uri  = [System.Web.HttpUtility]::UrlEncode($redirectUrl)
        }
        if($Refresh) { $queryHash.Add("prompt","refresh_session") }
        $phase1auth = Show-OAuthWindow -Url ($endpointUriAuthorize + (Convert-UriQueryFromHash $queryHash))

        # build authorization string with authentication code from web form auth
        $queryHash = [ordered]@{
            resource     = [System.Web.HttpUtility]::UrlEncode($resourceUri)
            client_id    = "$($ClientId)"
            grant_type   = "authorization_code"
            code         = "$($phase1auth.code)"
            redirect_uri = "$($redirectUrl)"
        }
        $authorizationPostRequest = Convert-UriQueryFromHash $queryHash -NoQuestionmark
    }
    else {
        # build authorization string with plain text credentials
        # Info https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-oauth2-client-creds-grant-flow#request-an-access-token
        Write-PSFMessage -Level Verbose -Message "Authentication is done by specified credentials. (No TwoFactor-Authentication supported!)" -Tag "Authorization"

        $queryHash = [ordered]@{
            resource   = [System.Web.HttpUtility]::UrlEncode($resourceUri)
            client_id  = $ClientId
            grant_type = "password"
            username   = $Credential.UserName
            password   = $Credential.GetNetworkCredential().password
        }
        $authorizationPostRequest = Convert-UriQueryFromHash $queryHash -NoQuestionmark
    }

    # Request an access token
    $content = New-Object System.Net.Http.StringContent($authorizationPostRequest, [System.Text.Encoding]::UTF8, "application/x-www-form-urlencoded")
    $clientResult = $httpClient.PostAsync([Uri]($endpointUriToken), $content)
    if($clientResult.Result.StatusCode -eq [System.Net.HttpStatusCode]"OK") {
        Write-PSFMessage -Level Verbose -Message "AccessToken granted. $($clientResult.Result.StatusCode.value__) ($($clientResult.Result.StatusCode)) $($clientResult.Result.ReasonPhrase)" -Tag "Authorization"
    }
    else {
        Stop-PSFFunction -Message "Request for AccessToken failed. $($clientResult.Result.StatusCode.value__) ($($clientResult.Result.StatusCode)) $($clientResult.Result.ReasonPhrase)" -Tag "Authorization" -EnableException $true
    }
    $jsonResponse = ConvertFrom-Json -InputObject $clientResult.Result.Content.ReadAsStringAsync().Result

    # Build output object
    $resultObject = New-Object MSGraph.Core.AzureAccessToken -Property @{
        TokenType      = $jsonResponse.token_type
        Scope          = $jsonResponse.scope -split " "
        ValidUntilUtc  = $baselineTimestamp.AddSeconds($jsonResponse.expires_on).ToUniversalTime()
        ValidFromUtc   = $baselineTimestamp.AddSeconds($jsonResponse.not_before).ToUniversalTime()
        ValidUntil     = New-Object DateTime($baselineTimestamp.AddSeconds($jsonResponse.expires_on).Ticks)
        ValidFrom      = New-Object DateTime($baselineTimestamp.AddSeconds($jsonResponse.not_before).Ticks)
        AccessToken    = $null
        RefreshToken   = $null
        IDToken        = $null
        Credential     = $Credential
        ClientId       = $ClientId
        Resource       = $resourceUri
        AppRedirectUrl = $RedirectUrl
    }
    # Insert token data into output object. done as secure string to prevent text output of tokens
    if ($jsonResponse.psobject.Properties.name -contains "refresh_token") { $resultObject.RefreshToken = ($jsonResponse.refresh_token | ConvertTo-SecureString -AsPlainText -Force) }
    if ($jsonResponse.psobject.Properties.name -contains "id_token") { $resultObject.IDToken = ($jsonResponse.id_token | ConvertTo-SecureString -AsPlainText -Force) }
    if ($jsonResponse.psobject.Properties.name -contains "access_token") {
        $resultObject.AccessToken = ($jsonResponse.access_token | ConvertTo-SecureString -AsPlainText -Force)
        $resultObject.AccessTokenInfo = ConvertFrom-JWTtoken -Token $jsonResponse.access_token
    }
    if ((Get-Date).IsDaylightSavingTime()) {
        $resultObject.ValidUntil = $resultObject.ValidUntil.AddHours(1)
        $resultObject.ValidFrom = $resultObject.ValidFrom.AddHours(1)
    }

    if($resultObject.IsValid) {
        if ($Register) {
            $script:msgraph_Token = $resultObject
            if($PassThru) { $resultObject }
        }
        else {
            $resultObject
        }
    }
    else {
        Stop-PSFFunction -Message "Token failure. Acquired token is not valid" -EnableException -Tag "Authorization"
    }
}

function Register-MgaAccessToken {
    <#
    .SYNOPSIS
        Registers an access token
 
    .DESCRIPTION
        Registers an access token, so all subsequent calls to Exchange Online reuse it by default.
 
    .PARAMETER Token
        The Token to register as default token for subsequent calls.
 
    .PARAMETER PassThru
        Outputs the token to the console
 
    .EXAMPLE
        PS C:\> Get-MgaRegisteredAccessToken
 
        Output the registered access token
    #>

    [CmdletBinding (SupportsShouldProcess=$false,
                    ConfirmImpact='Medium')]
    [OutputType([MSGraph.Core.AzureAccessToken])]
    param (
        [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ValueFromRemainingArguments=$false)]
        [ValidateNotNullOrEmpty()]
        #[MSGraph.Core.AzureAccessToken]
        $Token,

        [switch]
        $PassThru

    )

    $script:msgraph_Token = $Token
    if($PassThru) {
        $script:msgraph_Token
    }
}

function Update-MgaAccessToken {
    <#
    .SYNOPSIS
        Updates an existing access token
 
    .DESCRIPTION
        Updates an existing access token for contacting the specified application endpoint as long
        as the token is still valid. Otherwise, a new access is called through New-MgaAccessToken.
 
    .PARAMETER Token
        The token object to renew.
 
    .PARAMETER Register
        Registers the renewed token, so all subsequent calls to Exchange Online reuse it by default.
 
    .PARAMETER PassThru
        Outputs the token to the console, even when the register switch is set
 
    .EXAMPLE
        PS C:\> New-MgaAccessToken -MailboxName 'max.musterman@contoso.com'
 
        Registers an application to run under 'max.mustermann@contoso.com'.
        Requires an interactive session with a user handling the web UI.
 
    .EXAMPLE
        PS C:\> New-MgaAccessToken -MailboxName 'max.musterman@contoso.com' -Credential $cred
 
        Generates a token to a session as max.mustermann@contoso.com under the credentials specified in $cred.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(DefaultParameterSetName="Default")]
    param (
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        #[MSGraph.Core.AzureAccessToken]
        $Token,

        [Parameter(ParameterSetName='Register')]
        [switch]
        $Register,

        [Parameter(ParameterSetName='Register')]
        [switch]
        $PassThru
    )

    begin {
    }

    process {
        if (-not $Token) {
            $Token = $script:msgraph_Token
            $Register = $true
        }
        if (-not $Token) { Stop-PSFFunction -Message "Not connected! Use New-MgaAccessToken to create a Token and either register it or specifs it." -EnableException $true -Category AuthenticationError -Cmdlet $PSCmdlet }

        if (-not $Token.IsValid) {
            Write-PSFMessage -Level Warning -Message "Token lifetime already expired and can't be newed. New authentication is required. Calling New-MgaAccessToken..." -Tag "Authorization"
            $paramsNewToken = @{
                ClientId = $Token.AccessTokenInfo.ApplicationID.Guid
                RedirectUrl = $Token.AppRedirectUrl
            }
            if ($Token.Credential) { $paramsNewToken.Add("Credential", $Token.Credential ) }
            if ($Register -or ($script:msgraph_Token.AccessTokenInfo.Payload -eq $Token.AccessTokenInfo.Payload) ) { $paramsNewToken.Add("Register", $true) }
            $resultObject = New-MgaAccessToken -PassThru @paramsNewToken
            if ($PassThru) { return $resultObject } else { return }
        }

        $resourceUri = "https://graph.microsoft.com"
        $endpointUri = "https://login.windows.net/common/oauth2"
        $endpointUriToken = "$($endpointUri)/token "

        $baselineTimestamp = [datetime]"1970-01-01Z00:00:00"
        $httpClient = New-HttpClient

        $queryHash = [ordered]@{
            grant_type    = "refresh_token"
            resource      = [System.Web.HttpUtility]::UrlEncode($resourceUri)
            client_id     = $Token.ClientId.Guid
            refresh_token = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Token.RefreshToken))
        }
        $authorizationPostRequest = Convert-UriQueryFromHash $queryHash -NoQuestionmark


        $content = New-Object System.Net.Http.StringContent($authorizationPostRequest, [System.Text.Encoding]::UTF8, "application/x-www-form-urlencoded")
        $clientResult = $httpClient.PostAsync([Uri]$endpointUriToken, $content)
        if ($clientResult.Result.StatusCode -eq [System.Net.HttpStatusCode]"OK") {
            Write-PSFMessage -Level Verbose -Message "AccessToken renewal successful. $($clientResult.Result.StatusCode.value__) ($($clientResult.Result.StatusCode)) $($clientResult.Result.ReasonPhrase)" -Tag "Authorization"
        }
        else {
            Stop-PSFFunction -Message "Failed to renew AccessToken! $($clientResult.Result.StatusCode.value__) ($($clientResult.Result.StatusCode)) $($clientResult.Result.ReasonPhrase)" -Tag "Authorization" -EnableException $true
        }
        $jsonResponse = ConvertFrom-Json -InputObject $clientResult.Result.Content.ReadAsStringAsync().Result

        # Build output object
        $resultObject = New-Object MSGraph.Core.AzureAccessToken -Property @{
            TokenType      = $jsonResponse.token_type
            Scope          = $jsonResponse.scope -split " "
            ValidUntilUtc  = $baselineTimestamp.AddSeconds($jsonResponse.expires_on).ToUniversalTime()
            ValidFromUtc   = $baselineTimestamp.AddSeconds($jsonResponse.not_before).ToUniversalTime()
            ValidUntil     = New-Object DateTime($baselineTimestamp.AddSeconds($jsonResponse.expires_on).Ticks)
            ValidFrom      = New-Object DateTime($baselineTimestamp.AddSeconds($jsonResponse.not_before).Ticks)
            AccessToken    = $null
            RefreshToken   = $null
            IDToken        = $null
            Credential     = $Token.Credential
            ClientId       = $Token.ClientId.Guid
            Resource       = $Token.Resource.ToString()
            AppRedirectUrl = $Token.AppRedirectUrl.ToString()
        }
        # Insert token data into output object. done as secure string to prevent text output of tokens
        if ($jsonResponse.psobject.Properties.name -contains "refresh_token") { $resultObject.RefreshToken = ($jsonResponse.refresh_token | ConvertTo-SecureString -AsPlainText -Force) }
        if ($jsonResponse.psobject.Properties.name -contains "id_token") { $resultObject.IDToken = ($jsonResponse.id_token | ConvertTo-SecureString -AsPlainText -Force) }
        if ($jsonResponse.psobject.Properties.name -contains "access_token") {
            $resultObject.AccessToken = ($jsonResponse.access_token | ConvertTo-SecureString -AsPlainText -Force)
            $resultObject.AccessTokenInfo = ConvertFrom-JWTtoken -Token $jsonResponse.access_token
        }
        if ((Get-Date).IsDaylightSavingTime()) {
            $resultObject.ValidUntil = $resultObject.ValidUntil.AddHours(1)
            $resultObject.ValidFrom = $resultObject.ValidFrom.AddHours(1)
        }

        if ($resultObject.IsValid) {
            if ($Register) {
                $script:msgraph_Token = $resultObject
                if ($PassThru) { $resultObject }
            }
            else {
                $resultObject
            }
        }
        else {
            Stop-PSFFunction -Message "Token failure. Acquired token is not valid" -EnableException -Tag "Authorization"
        }
    }

    end {
    }
}

function Export-MgaMailAttachment {
    <#
    .SYNOPSIS
        Export a mail attachment to a file
 
    .DESCRIPTION
        Export/saves a mail attachment to a file
 
    .PARAMETER Path
        The directory where to export the attachment
 
    .PARAMETER InputObject
        The attachment object to export
 
    .EXAMPLE
        PS C:\> Export-MgaMailAttachment -InputObject $attachment -Path "$HOME"
 
        Export the attement to the users profile base directory
    #>

    [CmdletBinding()]
    [Alias('Save-MgaMailAttachment')]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ValueFromRemainingArguments=$false)]
        [ValidateNotNullOrEmpty()]
        [MSGraph.Exchange.Mail.Attachment]
        $InputObject,

        [String]
        $Path = (Get-Location).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 Message
        The display name of the folder to search.
        Defaults to the inbox.
 
    .PARAMETER Name
        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 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-MgaAccessToken.
        Can be omitted if a connection has been registered using the -Register parameter on New-MgaAccessToken.
 
    .EXAMPLE
        PS C:\> MgaMailMessage | Get-MgaMailAttachment
 
        Return all emails attachments in the inbox of the user connected to through a token.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    [OutputType([MSGraph.Exchange.Mail.Attachment])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByInputObject', Position = 0)]
        [Alias('Id', 'Mail', 'MailMessage', 'MessageId', 'MailId')]
        [MSGraph.Exchange.Mail.MessageParameter[]]
        $Message,

        [Parameter(Position = 1)]
        [string]
        $Name = "*",

        [string]
        $User,

        [switch]
        $IncludeInlineAttachment,

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

        [MSGraph.Core.AzureAccessToken]
        $Token
    )
    begin {
    }

    process {
        foreach ($item in $Message) {
            if($item.Name -and (-not $item.Id)) {
                Write-PSFMessage -Level Warning -Message "'$($item.Name)' has no valid ID to query. You have to input message objects or message Id to query attachments." -Tag "ParameterSetHandling"
                continue
            }
            $invokeParam = @{
                "Field"        = "messages/$($item.Id)/attachments"
                "Token"        = $Token
                "User"         = Resolve-UserString -User $User
                "ResultSize"   = $ResultSize
                "FunctionName" = $MyInvocation.MyCommand
            }

            Write-PSFMessage -Level Verbose -Message "Getting attachment from mail" -Tag "QueryData"

            $data = Invoke-MgaGetMethod @invokeParam | Where-Object { $_.name -like $Name }
            if (-not $IncludeInlineAttachment) { $data = $data | Where-Object isInline -eq $false}
            foreach ($output in $data) {
                [MSGraph.Exchange.Mail.Attachment]@{
                    BaseObject           = $output
                    Id                   = $output.Id
                    Name                 = $output.Name
                    ContentType          = $output.ContentType
                    ContentId            = $output.ContentId
                    ContentLocation      = $output.ContentLocation
                    IsInline             = $output.isInline
                    LastModifiedDateTime = $output.LastModifiedDateTime
                    Size                 = $output.Size
                }
            }
        }
    }

    end {
    }
}

function Get-MgaMailFolder {
    <#
    .SYNOPSIS
        Searches mail folders in Exchange Online
 
    .DESCRIPTION
        Searches mail folders in Exchange Online
 
    .PARAMETER Name
        The name of the folder(S) to query.
 
    .PARAMETER IncludeChildFolders
        Output all subfolders on queried folder(s).
 
    .PARAMETER Recurse
        Iterates through the whole folder structure and query all subfolders.
 
    .PARAMETER Filter
        The name to filter by.
        (Client Side filtering)
 
    .PARAMETER User
        The user-account to access. Defaults to the main user connected as.
        Can be any primary email name of any user the connected token has access to.
 
    .PARAMETER ResultSize
        The user to execute this under.
        Defaults to the user the token belongs to.
 
    .PARAMETER Token
        The token representing an established connection to the Microsoft Graph Api.
        Can be created by using New-MgaAccessToken.
        Can be omitted if a connection has been registered using the -Register parameter on New-MgaAccessToken.
 
    .EXAMPLE
        PS C:\> Get-MgaMailFolder
 
        Returns all folders in the mailbox of the connected user.
 
    .EXAMPLE
        PS C:\> Get-MgaMailFolder -Name Inbox
 
        Returns the "wellknown" inbox folder in the mailbox of the connected user.
        The wellknown folders can be specified by tab completion.
 
    .EXAMPLE
        PS C:\> Get-MgaMailFolder -Name Inbox -IncludeChildFolders
 
        Returns inbox and the next level of subfolders in the inbox of the connected user.
 
    .EXAMPLE
        PS C:\> Get-MgaMailFolder -Name Inbox -Recurse
 
        Returns inbox and the all subfolders underneath the inbox of the connected user.
        This one is like the "-Recurse" switch on the dir/Get-ChildItem command.
 
    .EXAMPLE
        PS C:\> Get-MgaMailFolder -Filter "My*" -User "max.master@contoso.onmicrosoft.com" -Token $Token
 
        Retrieves all folders where name starts with My in the mailbox of "max.master@contoso.onmicrosoft.com", using the connection token stored in $Token.
 
    .EXAMPLE
        PS C:\> Get-MgaMailFolder -ResultSize 5
 
        Retrieves only the first 5 folders in the mailbox of the connected user.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    [OutputType([MSGraph.Exchange.Mail.Folder])]
    param (
        [Parameter(ParameterSetName = 'ByFolderName', ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true, Position = 0)]
        [Alias('FolderName', 'InputObject', 'DisplayName', 'Id')]
        [MSGraph.Exchange.Mail.FolderParameter[]]
        $Name,

        [switch]
        $IncludeChildFolders,

        [switch]
        $Recurse,

        [string]
        $Filter = "*",

        [string]
        $User,

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

        [MSGraph.Core.AzureAccessToken]
        $Token
    )

    begin {
        if ($Recurse) { $IncludeChildFolders = $true }

        function invoke-internalMgaGetMethod ($invokeParam, [int]$level, [MSGraph.Exchange.Mail.Folder]$parentFolder) {
            $folderData = Invoke-MgaGetMethod @invokeParam | Where-Object displayName -Like $Filter
            foreach ($folderOutput in $folderData) {
                $hash = @{
                    Id                  = $folderOutput.Id
                    DisplayName         = $folderOutput.DisplayName
                    ParentFolderId      = $folderOutput.ParentFolderId
                    ChildFolderCount    = $folderOutput.ChildFolderCount
                    UnreadItemCount     = $folderOutput.UnreadItemCount
                    TotalItemCount      = $folderOutput.TotalItemCount
                    User                = $folderOutput.User
                    HierarchyLevel      = $level
                }
                if($parentFolder) { $hash.Add("ParentFolder", $parentFolder) }
                $folderOutputObject = New-Object -TypeName MSGraph.Exchange.Mail.Folder -Property $hash
                $folderOutputObject
            }
        }

        function get-childfolder ($output, $level, $invokeParam){
            $FoldersWithChilds = $output | Where-Object ChildFolderCount -gt 0
            $childFolders = @()

            do {
                $level = $level + 1
                foreach ($folderItem in $FoldersWithChilds) {
                    if($folderItem.ChildFolderCount -gt 0) {
                        Write-PSFMessage -Level VeryVerbose -Message "Getting childfolders for folder '$($folderItem.Name)'" -Tag "QueryData"
                        $invokeParam.Field = "mailFolders/$($folderItem.Id)/childFolders"
                        $childFolderOutput = invoke-internalMgaGetMethod -invokeParam $invokeParam -level $level -parentFolder $folderItem

                        $FoldersWithChilds = $childFolderOutput | Where-Object ChildFolderCount -gt 0
                        $childFolders = $childFolders + $childFolderOutput
                    }
                }
            } while ($Recurse -and $FoldersWithChilds)

            $childFolders
        }
    }

    process {
        Write-PSFMessage -Level VeryVerbose -Message "Gettings folder(s) by parameter set $($PSCmdlet.ParameterSetName)" -Tag "ParameterSetHandling"
        switch ($PSCmdlet.ParameterSetName) {
            "Default" {
                $level = 1
                $invokeParam = @{
                    "Field"        = 'mailFolders'
                    "Token"        = $Token
                    "User"         = Resolve-UserString -User $User
                    "ResultSize"   = $ResultSize
                    "FunctionName" = $MyInvocation.MyCommand
                }

                $output = invoke-internalMgaGetMethod -invokeParam $invokeParam -level $level

                if ($output -and $IncludeChildFolders) {
                    $childFolders = $output | Where-Object ChildFolderCount -gt 0 | ForEach-Object {
                        get-childfolder -output $_ -level $level -invokeParam $invokeParam
                    }
                    if($childFolders) {
                        [array]$output = [array]$output + $childFolders
                    }
                }
                $output
            }
            "ByFolderName" {
                foreach ($folder in $Name) {
                    $level = 1
                    Write-PSFMessage -Level VeryVerbose -Message "Getting folder '$( if($folder.Name){$folder.Name}else{$folder.Id} )'" -Tag "ParameterSetHandling"
                    $invokeParam = @{
                        "Token"        = $Token
                        "User"         = Resolve-UserString -User $User
                        "ResultSize"   = $ResultSize
                        "FunctionName" = $MyInvocation.MyCommand
                    }
                    if($folder.id) {
                        $invokeParam.add("Field", "mailFolders/$($folder.Id)")
                    }
                    else {
                        $invokeParam.add("Field", "mailFolders?`$filter=DisplayName eq '$($folder.Name)'")
                    }

                    $output = invoke-internalMgaGetMethod -invokeParam $invokeParam -level $level

                    if ($output -and $IncludeChildFolders) {
                        $childFolders = get-childfolder -output $output -level $level -invokeParam $invokeParam
                        if($childFolders) {
                            [array]$output = [array]$output + $childFolders
                        }
                    }
                    $output
                }

            }
            Default { stop-PSFMessage -Message "Unhandled parameter set. ($($PSCmdlet.ParameterSetName)) Developer mistage." -EnableException $true -Category "ParameterSetHandling" -FunctionName $MyInvocation.MyCommand }
        }
    }

    end {
    }
}

function Get-MgaMailMessage {
    <#
    .SYNOPSIS
        Retrieves messages from a email folder from Exchange Online using the graph api.
 
    .DESCRIPTION
        Retrieves messages from a email folder from Exchange Online using the graph api.
 
    .PARAMETER InputObject
        Carrier object for Pipeline input
        Accepts messages or folders from other Mga-functions
 
    .PARAMETER FolderName
        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 -Name "Junkemail" | Get-MgaMailMessage
 
        Return emails from the Junkemail folder of the user connected to through a token.
 
        .EXAMPLE
        PS C:\> Get-MgaMailMessage -FolderName "MyFolder" -Subject "Important*"
 
        Return emails where the subject starts with "Important" from the folder "MyFolder" of the user connected to through a token.
#>

    [CmdletBinding(DefaultParameterSetName = 'ByInputObject')]
    [OutputType([MSGraph.Exchange.Mail.Message])]
    param (
        [Parameter(ParameterSetName = 'ByInputObject', Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Input', 'Id')]
        [MSGraph.Exchange.Mail.MessageOrFolderParameter[]]
        $InputObject,

        [Parameter(ParameterSetName = 'ByFolderName', Position = 0)]
        [Alias('FolderId', 'Folder')]
        [string[]]
        $FolderName,

        [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"
        if ($PSCmdlet.ParameterSetName -like "ByInputObject" -and -not $InputObject) {
            Write-PSFMessage -Level Verbose -Message "No InputObject specified. Gettings mail from default folder (inbox)." -Tag "ParameterSetHandling"
            [MSGraph.Exchange.Mail.MessageOrFolderParameter]$InputObject = [MSGraph.Exchange.Mail.WellKnownFolder]::Inbox.ToString()
        }
        if ($PSCmdlet.ParameterSetName -like "ByFolderName") {
            foreach ($folderItem in $FolderName) {
                $folderItem = [MSGraph.Exchange.Mail.MessageOrFolderParameter]$folderItem
                if($folderItem.Name -and (-not $folderItem.IsWellKnownName)) {
                    [MSGraph.Exchange.Mail.MessageOrFolderParameter]$folderItem = Get-MgaMailFolder -Name $folderItem.Name -User $User -Token $Token
                }
                $InputObject = $InputObject + $folderItem
            }
        }

        foreach ($InputObjectItem in $InputObject) {
            Write-PSFMessage -Level VeryVerbose -Message "Parsing input $($InputObjectItem.TypeName) object '$($InputObjectItem)'"
            switch ($InputObjectItem.TypeName) {
                "MSGraph.Exchange.Mail.Message" {
                    if ($Delta -and ('@odata.deltaLink' -in $InputObjectItem.InputObject.BaseObject.psobject.Properties.Name)) {
                        # if delta message, construct a delta query from mail
                        Write-PSFMessage -Level VeryVerbose -Message "Delta parameter specified and delta message found. Checking on message '$($InputObjectItem)' from the pipeline"
                        $invokeParam = @{
                            "deltaLink"    = $InputObjectItem.InputObject.BaseObject.'@odata.deltaLink'
                            "Token"        = $Token
                            "ResultSize"   = $ResultSize
                            "FunctionName" = $MyInvocation.MyCommand
                        }
                    }
                    else {
                        # if non delta message is parsed in, the message will be queried again (refreshed)
                        # Not really necessary, but works as intend from pipeline usage
                        Write-PSFMessage -Level VeryVerbose -Message "Refresh message '$($InputObjectItem)' from the pipeline"
                        $invokeParam = @{
                            "Field"        = "messages/$($InputObjectItem.Id)"
                            "User"         = $InputObjectItem.InputObject.BaseObject.User
                            "Token"        = $Token
                            "ResultSize"   = $ResultSize
                            "FunctionName" = $MyInvocation.MyCommand
                        }
                        if ($Delta) { $invokeParam.Add("Delta", $true) }
                    }
                    $invokeParams = $invokeParams + $invokeParam
                }

                "MSGraph.Exchange.Mail.Folder" {
                    Write-PSFMessage -Level VeryVerbose -Message "Gettings messages in folder '$($InputObjectItem)' from the pipeline"
                    $invokeParam = @{
                        "Field"        = "mailFolders/$($InputObjectItem.Id)/messages"
                        "User"         = $InputObjectItem.InputObject.User
                        "Token"        = $Token
                        "ResultSize"   = $ResultSize
                        "FunctionName" = $MyInvocation.MyCommand
                    }
                    if ($Delta) { $invokeParam.Add("Delta", $true) }
                    $invokeParams = $invokeParams + $invokeParam
                }

                "System.String" {
                    $invokeParam = @{
                        "User"         = $User
                        "Token"        = $Token
                        "ResultSize"   = $ResultSize
                        "FunctionName" = $MyInvocation.MyCommand
                    }
                    if ($Delta) { $invokeParam.Add("Delta", $true) }

                    $name = if ($InputObjectItem.IsWellKnownName) { $InputObjectItem.Name } else { $InputObjectItem.Id }
                    if($name.length -eq 152) {
                        # Id is a message
                        Write-PSFMessage -Level VeryVerbose -Message "Gettings messages with Id '$($InputObjectItem)'" -Tag "InputValidation"
                        $invokeParam.Add("Field","messages/$($name)")
                    }
                    elseif ($name.length -eq 120)
                    {
                        # Id is a folder
                        Write-PSFMessage -Level VeryVerbose -Message "Gettings messages in folder with Id '$($InputObjectItem)'" -Tag "InputValidation"
                        $invokeParam.Add("Field","mailFolders/$($name)/messages")
                    }
                    elseif ($InputObjectItem.IsWellKnownName -and $name) {
                        # a well known folder is specified by name
                        $invokeParam.Add("Field","mailFolders/$($name)/messages")
                    }
                    else {
                        # not a valid Id -> should not happen
                        Write-PSFMessage -Level Warning -Message "The specified Id seeams not be a valid Id. Skipping object '$($name)'" -Tag "InputValidation"
                        continue
                    }

                    $invokeParams = $invokeParams + $invokeParam
                    Remove-Variable -Name name -Force -ErrorAction Ignore -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false
                }

                Default { Write-PSFMessage -Level Critical -Message "Failed on type validation. Can not handle $($InputObjectItem.TypeName)" -EnableException $true -Tag "TypeValidation" }
            }
        }
    }

    end {
        $fielList = @()
        $InvokeParamsUniqueList = @()
        Write-PSFMessage -Level Verbose -Message "Checking $( ($InvokeParams | Measure-Object).Count ) objects on unique calls..."
        foreach ($invokeParam in $InvokeParams) {
            if ($invokeParam.Field -and ($invokeParam.Field -notin $fielList)) {
                $InvokeParamsUniqueList = $InvokeParamsUniqueList + $invokeParam
                $fielList = $fielList + $invokeParam.Field
            }
            elseif ($invokeParam.deltaLink -notin $fielList) {
                $InvokeParamsUniqueList = $InvokeParamsUniqueList + $invokeParam
                $fielList = $fielList + $invokeParam.deltaLink
            }
        }
        Write-PSFMessage -Level Verbose -Message "Invoking $( ($InvokeParamsUniqueList | Measure-Object).Count ) REST calls for gettings messages"

        # run the message query and process the output
        foreach ($invokeParam in $InvokeParamsUniqueList) {
            $data = Invoke-MgaGetMethod @invokeParam | Where-Object { $_.subject -like $Subject }
            foreach ($output in $data) {
                New-MgaMailMessageObject -RestData $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 Message
        Carrier object for Pipeline input. Accepts messages and strings.
 
    .PARAMETER DestinationFolder
        The destination folder where to move the message to
 
        .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 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.
 
    .PARAMETER PassThru
        Outputs the token to the console
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .EXAMPLE
        PS C:\> $mails | Move-MgaMailMessage -DestinationFolder $destinationFolder
 
        Moves messages in variable $mails to the folder in the variable $destinationFolder.
        also possible:
        PS C:\> Move-MgaMailMessage -Message $mails -DestinationFolder $destinationFolder
 
        The variable $mails can be represent:
        PS C:\> $mails = Get-MgaMailMessage -Folder Inbox -ResultSize 1
 
        The variable $destinationFolder can be represent:
        PS C:\> $destinationFolder = Get-MgaMailFolder -Name "Archive"
 
    .EXAMPLE
        PS C:\> Move-MgaMailMessage -Id $mails.id -DestinationFolder $destinationFolder
 
        Moves messages into the folder $destinationFolder.
 
        The variable $mails can be represent:
        PS C:\> $mails = Get-MgaMailMessage -Folder Inbox -ResultSize 1
 
        The variable $destinationFolder can be represent:
        PS C:\> $destinationFolder = Get-MgaMailFolder -Name "Archive"
 
    .EXAMPLE
        PS C:\> Get-MgaMailMessage -Folder Inbox | Move-MgaMailMessage -DestinationFolder $destinationFolder
 
        Moves ALL messages from your inbox into the folder $destinationFolder.
        The variable $destinationFolder can be represent:
        PS C:\> $destinationFolder = Get-MgaMailFolder -Name "Archive"
 
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium', DefaultParameterSetName = 'Default')]
    [Alias()]
    [OutputType([MSGraph.Exchange.Mail.Message])]
    param (
        [Parameter(Mandatory=$true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('InputObject', 'MessageId', 'Id', 'Mail', 'MailId')]
        [MSGraph.Exchange.Mail.MessageParameter[]]
        $Message,

        [Parameter(Mandatory = $true, Position = 1)]
        [MSGraph.Exchange.Mail.FolderParameter]
        $DestinationFolder,

        [string]
        $User,

        [MSGraph.Core.AzureAccessToken]
        $Token,

        [switch]
        $PassThru
    )
    begin {
        if($DestinationFolder.TypeName -like "System.String") {
            [MSGraph.Exchange.Mail.FolderParameter]$DestinationFolder = Get-MgaMailFolder -Name $DestinationFolder.ToString() -User $User -Token $Token
        }

        $bodyHash = @{
            destinationId = ($DestinationFolder.Id | ConvertTo-Json)
        }
    }

    process {
        Write-PSFMessage -Level Debug -Message "Gettings messages by parameter set $($PSCmdlet.ParameterSetName)" -Tag "ParameterSetHandling"

        #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 ($messageItem in $Message) {
            if ($messageItem.TypeName -like "System.String") {
                if ($messageItem.Id -and ($messageItem.Id.Length -eq 152)) {
                    [MSGraph.Exchange.Mail.MessageParameter]$messageItem = Get-MgaMailMessage -InputObject $messageItem.Id -User $User -Token $Token
                }
                else {
                    Write-PSFMessage -Level Warning -Message "The specified input string seams not to be a valid Id. Skipping object '$($messageItem)'" -Tag "InputValidation"
                    continue
                }
            }

            if ($User -and ($messageItem.TypeName -like "MSGraph.Exchange.Mail.Message") -and ($User -notlike $messageItem.InputObject.BaseObject.User)) {
                Write-PSFMessage -Level Important -Message "Individual user specified with message object! User from message object ($($messageItem.InputObject.BaseObject.User))will take precedence on specified user ($($User))!" -Tag "InputValidation"
                $User = $messageItem.InputObject.BaseObject.User
            }
            elseif ((-not $User) -and ($messageItem.TypeName -like "MSGraph.Exchange.Mail.Message")) {
                $User = $messageItem.InputObject.BaseObject.User
            }

            if ($pscmdlet.ShouldProcess("message '$($messageItem)'", "Move to folder '$($DestinationFolder.Name)'")) {
                Write-PSFMessage -Tag "MessageUpdate" -Level Verbose -Message "Move message '$($messageItem)' to folder '$($DestinationFolder)'"
                $invokeParam = @{
                    "Field"        = "messages/$($messageItem.Id)/move"
                    "User"         = $User
                    "Body"         = $bodyJSON
                    "ContentType"  = "application/json"
                    "Token"        = $Token
                    "FunctionName" = $MyInvocation.MyCommand
                }
                $output = Invoke-MgaPostMethod @invokeParam
                if ($PassThru) { New-MgaMailMessageObject -RestData $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 Message
        Carrier object for Pipeline input. Accepts messages.
 
    .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-MgaAccessToken.
        Can be omitted if a connection has been registered using the -Register parameter on New-MgaAccessToken.
 
    .PARAMETER PassThru
        Outputs the token to the console
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .EXAMPLE
        PS C:\> $mail | Set-MgaMailMessage -IsRead $false
 
        Set messages represented by variable $mail to status "unread"
        The variable $mails can be represent:
        PS C:\> $mails = Get-MgaMailMessage -Folder Inbox -ResultSize 1
 
    .EXAMPLE
        PS C:\> $mail | Set-MgaMailMessage -IsRead $false -categories "Red category"
 
        Set status "unread" and category "Red category" to messages represented by variable $mail
        The variable $mails can be represent:
        PS C:\> $mails = Get-MgaMailMessage -Folder Inbox -ResultSize 1
 
    .EXAMPLE
        PS C:\> $mail | Set-MgaMailMessage -ToRecipients "someone@something.org"
 
        Set reciepent from draft mail represented by variable $mail
        The variable $mails can be represent:
        PS C:\> $mails = Get-MgaMailMessage -Folder Drafts
 
    .EXAMPLE
        PS C:\> Set-MgaMailMessage -Id $mail.Id -ToRecipients "someone@something.org" -Subject "Something important"
 
        Set reciepent from draft mail represented by variable $mail
        The variable $mails can be represent:
        PS C:\> $mails = Get-MgaMailMessage -Folder Drafts
 
    .EXAMPLE
        PS C:\> $mail | Set-MgaMailMessage -ToRecipients $null
 
        Clear reciepent from draft mail represented by variable $mail
        The variable $mails can be represent:
        PS C:\> $mails = Get-MgaMailMessage -Folder Drafts
 
 
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium', DefaultParameterSetName = 'Default')]
    [Alias("Update-MgaMailMessage")]
    [OutputType([MSGraph.Exchange.Mail.Message])]
    param (
        [Parameter(Mandatory=$true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('InputObject', 'MessageId', 'Id')]
        [MSGraph.Exchange.Mail.MessageParameter[]]
        $Message,

        [string]
        $User,

        [ValidateNotNullOrEmpty()]
        [bool]
        $IsRead,

        [string]
        $Subject,

        [AllowNull()]
        [AllowEmptyCollection()]
        [AllowEmptyString()]
        [string]
        $Sender,

        [AllowNull()]
        [AllowEmptyCollection()]
        [AllowEmptyString()]
        [string]
        $From,

        [AllowNull()]
        [AllowEmptyCollection()]
        [AllowEmptyString()]
        [string[]]
        $ToRecipients,

        [AllowNull()]
        [AllowEmptyCollection()]
        [AllowEmptyString()]
        [string[]]
        $CCRecipients,

        [AllowNull()]
        [AllowEmptyCollection()]
        [AllowEmptyString()]
        [string[]]
        $BCCRecipients,

        [AllowNull()]
        [AllowEmptyCollection()]
        [AllowEmptyString()]
        [string[]]
        $ReplyTo,

        [String]
        $Body,

        [String[]]
        $Categories,

        [ValidateSet("Low", "Normal", "High")]
        [String]
        $Importance,

        [ValidateSet("focused", "other")]
        [String]
        $InferenceClassification,

        [String]
        $InternetMessageId,

        [bool]
        $IsDeliveryReceiptRequested,

        [bool]
        $IsReadReceiptRequested,

        [MSGraph.Core.AzureAccessToken]
        $Token,

        [switch]
        $PassThru
    )
    begin {
        $boundParameters = @()
        $mailAddressNames = @("sender", "from", "toRecipients", "ccRecipients", "bccRecipients", "replyTo")

        # parsing mailAddress parameter strings to mailaddress objects (if not empty)
        foreach ($Name in $mailAddressNames) {
            if (Test-PSFParameterBinding -ParameterName $name) {
                New-Variable -Name "$($name)Addresses" -Force -Scope 0
                if( (Get-Variable -Name $Name -Scope 0).Value ) {
                    try {
                        Set-Variable -Name "$($name)Addresses" -Value ( (Get-Variable -Name $Name -Scope 0).Value | ForEach-Object { [mailaddress]$_ } -ErrorAction Stop -ErrorVariable parseError )
                    }
                    catch {
                        Stop-PSFFunction -Message "Unable to parse $($name) to a mailaddress. String should be 'name@domain.topleveldomain' or 'displayname name@domain.topleveldomain'. Error: $($parseError[0].Exception)" -Tag "ParameterParsing" -Category InvalidData -EnableException $true -Exception $parseError[0].Exception
                    }
                }
            }
        }
    }

    process {
        $bodyHash = @{}
        Write-PSFMessage -Level Debug -Message "Gettings messages by parameter set $($PSCmdlet.ParameterSetName)" -Tag "ParameterSetHandling"

        #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 ($messageItem in $Message) {
            if ($messageItem.TypeName -like "System.String") {
                if ($messageItem.Id -and ($messageItem.Id.Length -eq 152)) {
                    [MSGraph.Exchange.Mail.MessageParameter]$messageItem = Get-MgaMailMessage -InputObject $messageItem.Id -User $User -Token $Token
                }
                else {
                    Write-PSFMessage -Level Warning -Message "The specified input string seams not to be a valid Id. Skipping object '$($messageItem)'" -Tag "InputValidation"
                    continue
                }
            }

            if ($User -and ($messageItem.TypeName -like "MSGraph.Exchange.Mail.Message") -and ($User -notlike $messageItem.InputObject.BaseObject.User)) {
                Write-PSFMessage -Level Important -Message "Individual user specified with message object! User from message object ($($messageItem.InputObject.BaseObject.User))will take precedence on specified user ($($User))!" -Tag "InputValidation"
                $User = $messageItem.InputObject.BaseObject.User
            }
            elseif ((-not $User) -and ($messageItem.TypeName -like "MSGraph.Exchange.Mail.Message")) {
                $User = $messageItem.InputObject.BaseObject.User
            }

            if ($pscmdlet.ShouldProcess("message '$($messageItem)'", "Update properties '$([string]::Join("', '", $boundParameters))'")) {
                Write-PSFMessage -Tag "MessageUpdate" -Level Verbose -Message "Update properties '$([string]::Join("', '", $boundParameters))' on message '$($messageItem)'"
                $invokeParam = @{
                    "Field"        = "messages/$($messageItem.Id)"
                    "User"         = $User
                    "Body"         = $bodyJSON
                    "ContentType"  = "application/json"
                    "Token"        = $Token
                    "FunctionName" = $MyInvocation.MyCommand
                }
                $output = Invoke-MgaPatchMethod @invokeParam
                if ($PassThru) { New-MgaMailMessageObject -RestData $output }
            }
        }
        #endregion Update messages
    }

}

<#
This is an example configuration file
 
By default, it is enough to have a single one of them,
however if you have enough configuration settings to justify having multiple copies of it,
feel totally free to split them into multiple files.
#>


<#
# Example Configuration
Set-PSFConfig -Module 'MSGraph' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'"
#>


Set-PSFConfig -Module 'MSGraph' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging."
Set-PSFConfig -Module 'MSGraph' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments."

Set-PSFConfig -Module 'MSGraph' -Name 'Tenant.Application.ClientID' -Value "1930085c-c139-42f5-8d1a-0b9c88ca43e3" -Initialize -Validation 'string' -Description "Well known ClientID from registered Application in Azure tenant"
Set-PSFConfig -Module 'MSGraph' -Name 'Tenant.Application.RedirectUrl' -Value "https://localhost" -Initialize -Validation 'string' -Description "Redirection URL specified in MS Azure Application portal for the registered application"
Set-PSFConfig -Module 'MSGraph' -Name 'Query.ResultSize' -Value 100 -Initialize -Validation integer -Description "Limit of amount of records returned by a function. Use 0 for unlimited."
Set-PSFConfig -Module 'MSGraph' -Name 'Hierarchy.Path.Separator' -Value "\" -Initialize -Validation string -Description "the character used to process hierarchical names (like FullName property on folders) in MSGraph module."

<#
# Example:
Register-PSFTeppScriptblock -Name "MSGraph.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' }
#>



Register-PSFTeppScriptblock -Name "MSGraph.Exchange.Mail.WellKnowFolders" -ScriptBlock { [enum]::GetNames([MSGraph.Exchange.Mail.WellKnownFolder]) | ForEach-Object { (Get-Culture).TextInfo.ToTitleCase( $_ ) } }


<#
# Example:
Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name MSGraph.alcohol
#>


Register-PSFTeppArgumentCompleter -Command Get-MgaMailFolder -Parameter "Name" -Name "MSGraph.Exchange.Mail.WellKnowFolders"
Register-PSFTeppArgumentCompleter -Command Get-MgaMailMessage -Parameter "FolderName" -Name "MSGraph.Exchange.Mail.WellKnowFolders"
Register-PSFTeppArgumentCompleter -Command Move-MgaMailMessage -Parameter "DestinationFolder" -Name "MSGraph.Exchange.Mail.WellKnowFolders"


New-PSFLicense -Product 'MSGraph' -Manufacturer 'Friedrich Weinmann, Andreas Bellstedt' -ProductVersion $ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2018-08-28") -Text @"
Copyright (c) 2018 Friedrich Weinmann, Andreas Bellstedt
 
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