ExchangeOnlineArchiveTransfer.psm1

function Split-EOATMailItem {
    <#
    .SYNOPSIS
    Split the mail items into batches
     
    .DESCRIPTION
    This function will split the mail items into batches.
    The default batch size is 90 GB, which is also the recommended maximum batch size.
     
    .PARAMETER MailItems
    PSCustomObject array. Default value is $null.
    This parameter defines the mail items, which should be split into batches.
    You can use the function 'Get-EOATMailItem' to get a list of mail items.
     
    .PARAMETER MaxBatchSize
    Integer value. Default value is 96636764160 (90 GB).
    This parameter defines the maximum batch size in Byte, which is used to retrieve the mail items.
    If more than 90 GB of mail items are send to this function, the mails will be split into batches of 90 GB.
     
    .EXAMPLE
    Split-EOATMailItem -MailItems $MailItems -MaxBatchSize 90GB
 
    This example splits the mail items into batches of 90 GB.
 
    .EXAMPLE
    Split-EOATMailItem -MailItems $MailItems -MaxBatchSize 20GB
 
    This example splits the mail items into batches of 20 GB.
     
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.Generic.List[PSCustomObject]])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [PSCustomObject[]]
        $MailItems,

        [int]
        $MaxBatchSize = 90GB
    )

    begin {
        $batches = [System.Collections.Generic.List[PSCustomObject]]::new()
        $currentBatch = [System.Collections.Generic.List[PSCustomObject]]::new()
        $currentbatchSize = 0
    }
    process {
        foreach ($mailItem in $MailItems) {
            # add mail item to current batch
            $null = $currentBatch.Add($mailItem)
            # add mail item size to current batch size
            $currentbatchSize += $mailItem.Size

            # check if current batch size is greater than max batch size
            if ($currentbatchSize -ge $MaxBatchSize) {
                # add current batch to batches
                $null = $batches.Add($currentBatch)
                # reset current batch size
                $currentbatchSize = 0
                # reset current batch
                $currentBatch = [System.Collections.Generic.List[PSCustomObject]]::new()
            }
        }
    }
    end {
        # add the last batch to batches (if it is not empty), as it is not added in the foreach loop above, because the last batch is not greater than the max batch size
        if ($currentBatch.Count -gt 0) {
            $null = $batches.Add($currentBatch)
        }
        
        # return all batches
        $batches
    }
}

function Connect-EOATExchangeWebService {
    <#
    .SYNOPSIS
    Connect to Exchange Online EWS
     
    .DESCRIPTION
    Connect to Exchange Online EWS
    This function returns an EWS access token
     
    .PARAMETER ApplicationId
    Guid of the Azure AD application
     
    .PARAMETER TenantId
    Guid of the Azure AD tenant
 
    .PARAMETER MailboxName
    Name of the mailbox to connect to
     
    .PARAMETER Scopes
    Scope of the Azure AD application. Default value is 'https://outlook.office365.com/EWS.AccessAsUser.All'
 
    .PARAMETER PassThru
    Pass the EWS service object and token to the pipeline
     
    .EXAMPLE
    Connect-EOATExchangeWebService -ApplicationId '00000000-0000-0000-0000-000000000000' -TenantId '00000000-0000-0000-0000-000000000000' -MailboxName 'user@contoso.com'
 
    This example connects to Exchange Online EWS with the Azure AD application '00000000-0000-0000-0000-000000000000' and the Azure AD tenant '00000000-0000-0000-0000-000000000000' and the mailbox 'user@contoso.com'
 
    .EXAMPLE
 
    Connect-EOATExchangeWebService -ApplicationId '00000000-0000-0000-0000-000000000000' -TenantId '00000000-0000-0000-0000-000000000000' -MailboxName 'user@contoso.com' -PassThru
 
    $ewsObj, $tokenObj = This example connects to Exchange Online EWS with the Azure AD application '00000000-0000-0000-0000-000000000000' and the Azure AD tenant '00000000-0000-0000-0000-000000000000' and the mailbox 'user@contoso.com' and passes the EWS service object and token to the pipeline
 
 
    #>

    [CmdletBinding()]
    [OutputType([Microsoft.Exchange.WebServices.Data.ExchangeService])]
    param(
        [Parameter(Mandatory = $true)]
        [Guid]
        $ApplicationId,

        [Parameter(Mandatory = $true)]
        [Guid]
        $TenantId,

        [Parameter(Mandatory = $true)]
        [String]
        $MailboxName,

        [String]
        $Scopes = 'https://outlook.office365.com/EWS.AccessAsUser.All',

        [Switch]
        $PassThru
    )

    # Verbose output
    Write-Verbose -Message "Connecting to Exchange Online EWS with ApplicationId '$ApplicationId' and TenantId '$TenantId'"

    # Get token
    Write-Warning -Message "Please use the credentials of the source mailbox to connect to Exchange Online EWS. This mailbox must have FullAccess permission on the target mailbox as well, to be able to access the target mailbox."
    $ewsEntraService = @{
        Name          = 'ExoEwsService'
        ServiceUrl    = 'https://graph.microsoft.com/v1.0'
        Resource      = 'https://outlook.office365.com'
        DefaultScopes = @()
        HelpUrl       = ''
        Header        = @{}
        NoRefresh     = $false
    }
    $null = Register-EntraService @ewsEntraService
    $script:EwsToken = Connect-EntraService -ClientID $ApplicationId -TenantID $TenantId -Service Graph -Scopes $Scopes -DeviceCode -PassThru

    # create EWS service object
    $script:EwsService = [Microsoft.Exchange.WebServices.Data.ExchangeService]::new()
 
    #Use Modern Authentication
    $EwsService.Credentials = [Microsoft.Exchange.WebServices.Data.OAuthCredentials]$EwsToken.AccessToken
 
    #Check EWS connection
    $EwsService.Url = "https://outlook.office365.com/EWS/Exchange.asmx"
    Write-Verbose "Starting Autodiscover for $MailboxName"
    $EwsService.AutodiscoverUrl($MailboxName, { $true }) # use overload with callback
    #EWS connection is Success if no error returned.

    # set script variable for source mailbox
    $script:SourceMailbox = $MailboxName

    # Verbose output
    Write-Verbose -Message "Successfully connected to Exchange Online EWS"

    # Pass EWS service object and token to the pipeline
    if ($PassThru) {
        $EwsService
        $EwsToken
    }
}

function Copy-EOATMailItemToOtherMailbox {
    <#
    .SYNOPSIS
    Copy mail items to another mailbox
     
    .DESCRIPTION
    Copy mail items to another mailbox
    This function copys mail items to another mailbox using the Exchange Web Services (EWS) API.
     
    .PARAMETER MailItems
    PSCustomObject array. Default value is $null.
    This parameter defines the mail items, which should be copyd.
    You can use the function 'Get-EOATMailItem' to get a list of mail items.
     
    .PARAMETER TargetMailbox
    String value. Default value is $null.
    This parameter defines the target mailbox, to which the mail items should be copyd.
     
    .PARAMETER TargetFolder
    String value. Default value is $null.
    This parameter defines the target folder, to which the mail items should be copyd.
     
    .PARAMETER MaxBatchSize
    Integer value. Default value is 96636764160 (90 GB).
    This parameter defines the maximum batch size in Byte, which is used to retrieve the mail items.
    If more than 90 GB of mail items are send to this function, the mails will be split into batches minimum 90 GB.
    A single batch could be larger than 90GB, because we add items to the batch until AT LEAST 90GB are reached.
 
    .PARAMETER WaitTime
    Integer value. Default value is 300 (5 minutes).
    This parameter defines the time in seconds, which the function will wait, before continuing with the next batch.
 
    .PARAMETER CheckTargetFolderEmpty
    Switch value. Default value is $true.
    This parameter defines, if the function should check, if the target folder is empty, before continuing with the next batch.
    If the target folder is not empty, the function will wait $WaitTime seconds before continuing with the next batch.
    As soon as the target folder is empty, the function will ask the script user, if the function should continue with the next batch.
 
    .PARAMETER LogEnabled
    Switch value. Default value is $false.
    This parameter defines, if the function should log the mail item copy process to a CSV file.
 
    .PARAMETER LogFilePath
    String value. Default value is "$env:temp\Copy-EOATMailItemToOtherMailbox-$(Get-Date -Format yyyyMMddhhmmss).csv".
    This parameter defines the path to the log file, which is used to log the mail item copy process.
    The log file have the following header: SourceMailbox, SourceFolderId, TargetMailbox, TargetFolder, TargetFolderId, SourceMailItemId, Sender, Subject, Received, SizeInMB, CurrentWindowsUser
 
    .PARAMETER LogDelimiter
    String value. Default value is ';'.
    This parameter defines the delimiter, which is used to separate the values in the log file.
 
    .PARAMETER Service
    ExchangeService object. Default value is the script variable $script:EwsService.
    This parameter defines the ExchangeService object, which is used to connect to Exchange Online EWS.
    This parameter will be set automatically, if you use the function 'Connect-EOATExchangeWebService' to connect to Exchange Online EWS.
     
    .PARAMETER Confirm
    Switch value. Default value is $true.
    This parameter defines, if the function should ask the script user to continue with the next batch, if the target folder is empty.
 
    .PARAMETER WhatIf
    Switch value. Default value is $false.
    This parameter defines, if the function should simulate the execution of the function.
    This parameter is currently not implemented
 
    .EXAMPLE
    Copy-EOATMailItemToOtherMailbox -MailItems $MailItems -TargetMailbox 'temporary_mailbox@contoso.com' -TargetFolder 'Inbox'
     
    This example copys the mail items defined in the variable $MailItems to the mailbox 'temporary_mailbox@contoso.com'. The mail items will be copyd to the folder 'Inbox'.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject[]]
        $MailItems,

        [Parameter(Mandatory = $true)]
        [String]
        $TargetMailbox,

        [ArgumentCompleter({
                [Microsoft.Exchange.Webservices.Data.WellKnownFolderName] | Get-Member -Static -MemberType Properties | Select-Object -ExpandProperty Name
            })]
        [Parameter(Mandatory = $true)]
        [String]
        $TargetFolder,

        [Int64]
        $MaxBatchSize = 90GB,

        [Int32]
        $WaitTime = 300,

        [bool]
        $CheckTargetFolderEmpty = $true,

        [switch]
        $LogEnabled,

        [string]
        $LogFilePath = "$env:temp\Copy-EOATMailItemToOtherMailbox-$(Get-Date -Format yyyyMMddhhmmss).csv",

        [string]
        $LogDelimiter = ';',

        [Parameter(DontShow)]
        [Microsoft.Exchange.WebServices.Data.ExchangeServiceBase]
        $Service = $script:EwsService
    )

    Begin {
        # trap statement
        $ErrorActionPreference = 'Stop'
        trap {
            Write-Error -Message $_.Exception.Message
            Write-Error -Message $_.Exception.StackTrace
            return
        }
        
        # Check if target folder exists
        try {
            $targetFolderId = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service, [Microsoft.Exchange.WebServices.Data.FolderId]::new($TargetFolder, $TargetMailbox))
        }
        catch {
            Write-Error -Message "The target folder '$TargetFolder' does not exist in mailbox '$TargetMailbox' or you do not have FullAccess permission on the target mailbox."
            return
        }

        # Check, if we have to split the result array
        if (($MailItems.size | Measure-Object -Sum).Sum -gt $MaxBatchSize) {
            $mailItemBatches = Split-EOATMailItem -MailItems $MailItems -MaxBatchSize $MaxBatchSize
        }
        else {
            $mailItemBatches = [System.Collections.Generic.List[PSCustomObject]]::new()
            $null = $mailItemBatches.Add($MailItems)
        }

        # If LogEnabled, check if the log file already exists. If so, stop the script and ask the user to delete the log file.
        if($LogEnabled) {
            if (Test-Path -Path $LogFilePath) {
                Write-Error -Message "The file '$LogFilePath' already exists. Please delete the log file and try again."
                return
            }
        }
    }

    # Copy mail items
    Process {
        # Please beware, that you could copy mail items multipe times to the same target folder!!!
        foreach ($mailItemBatch in $mailItemBatches) {
            Write-Verbose -Message "Copy $($mailItemBatch.Count) mail items to folder '$TargetFolder' in mailbox '$TargetMailbox'"
            foreach ($mailItem in $mailItemBatch) {
                # Write progress
                $progressProps = @{
                    Activity        = "Batch $($mailItemBatches.IndexOf($mailItemBatch) + 1) of $($mailItemBatches.count)"
                    Status          = "Copying mail item '$($mailItem.Subject)' to folder '$TargetFolder' in mailbox '$TargetMailbox'"
                    PercentComplete = (($mailItemBatch.IndexOf($mailItem) + 1) / $mailItemBatch.Count * 100)
                }
                Write-Progress @progressProps
                
                $retryCount = 0
                $copySuccess = $false
                do {
                    # If the method fails three times with the error "The server cannot service this request right now. Try again later.", the function will return an error
                    if($retryCount -eq 3) {
                        Write-Error -Message "The method failed with error 'Try again later' three times. The function will return an error."
                        return
                    }

                    try {
                        # Copy mail item
                        $null = $mailItem.Copy($targetFolderId.Id)
                    }
                    catch {
                        if ($_.Exception.Message -like "*The server cannot service this request right now. Try again later.*") { # If the error message contains "The server cannot service this request right now. Try again later.", we will wait and retry the method
                            $retryCount++
                            $backOffMilliseconds = $_.Exception.InnerException.BackOffMilliseconds + 100
                            Write-Warning -Message "The method failed with error '$($_.Exception.Message)'. Waiting $backOffMilliseconds milliseconds before retrying the method."
                            Start-Sleep -Milliseconds $backOffMilliseconds
                            continue
                        }
                        elseif ($_.Exception.Message -like "*Unauthorized*") {
                            $retryCount++
                            Write-Warning -Message "The method failed with error '$($_.Exception.Message)'. Refreshing token and retrying the method."
                            # Refresh token
                            $script:EwsToken.RenewToken()

                            # Reconnect to EWS
                            $script:EwsService.Credentials = [Microsoft.Exchange.WebServices.Data.OAuthCredentials]$script:EwsToken.AccessToken
                            Write-Verbose "Starting Autodiscover for $MailboxName"
                            $script:EwsService.AutodiscoverUrl($MailboxName, { $true }) # use overload with callback
                            $Service = $script:EwsService

                            continue
                        }
                        else { # in other cases, we will throw the error
                            throw $_
                        }
                    }

                    # If the method was successful, we will set $copySuccess to $true
                    $copySuccess = $true
                } while (-not $copySuccess)

                if ($LogEnabled) {
                    # Create log object
                    $logObject = @{
                        SourceMailbox      = $script:SourceMailbox
                        # SourceFolder = '' # SourceFolder Name is not available on this object. Will be ignored for now for performance reasons. SourceFolderId can be used to get the SourceFolder Name using Get-EOATMailFolder.
                        SourceFolderId     = $mailItem.ParentFolderId.UniqueId
                        TargetMailbox      = $TargetMailbox
                        TargetFolder       = $TargetFolder
                        TargetFolderId     = $targetFolderId.Id
                        SourceMailItemId   = $mailItem.Id.UniqueId
                        Sender             = $mailItem.From.Address
                        Subject            = ''
                        Received           = $mailItem.DateTimeReceived.ToString('yyyy-MM-dd-hh-mm-ss')
                        SizeInMB           = "{0:N2}" -f ($mailItem.Size / 1024 / 1024)
                        CurrentWindowsUser = "$env:USERDOMAIN\$env:USERNAME"
                    }

                    # If the mail item has a subject, we will add the subject to the log object
                    if($null -ne $mailItem.Subject -and "" -ne $mailItem.Subject) {
                        $logObject.Subject = $mailItem.Subject
                    }

                    # Export log object to CSV file
                    $logObject | Export-Csv -Path $LogFilePath -Append -NoTypeInformation -Encoding utf8 -Delimiter $LogDelimiter
                }
            }

            # if we had only one batch or if we are at the last batch, we will not ask the user to continue with the next batch
            if (($mailItemBatches.count -eq 1) -or ($mailItemBatches.IndexOf($mailItemBatch) -eq ($mailItemBatches.count - 1))) {
                Write-Verbose -Message "No more batches to process."
                return
            }
                
            # Check the target folder count every x seconds before continuing with the next batch, to prevent the mailbox from exceeding the quota
            # Ask the script user if we should continue, as soon as the target folder is empty
            if($CheckTargetFolderEmpty) {
                $targetFolderItemcount = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service, [Microsoft.Exchange.WebServices.Data.FolderId]::new($TargetFolder, $TargetMailbox)) | Select-Object -ExpandProperty TotalCount
                while ($targetFolderItemcount -ne 0) {
                    Write-Warning -Message "Target folder '$TargetFolder' in mailbox '$TargetMailbox' is not empty. Waiting $WaitTime seconds before continuing with the next batch."
                    Start-Sleep -Seconds $WaitTime
                    $targetFolderItemcount = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service, [Microsoft.Exchange.WebServices.Data.FolderId]::new($TargetFolder, $TargetMailbox)) | Select-Object -ExpandProperty TotalCount
                }
            }

            # If confirm is set to false, we will continue with the next batch immediately
            if ($ConfirmPreference -eq "None") {
                continue
            }
        
            # Ask the script user if we should continue, if the target folder is empty
            $continue = $false
            while ($continue -eq $false) {
                $continuationPrompt = ""
                if($CheckTargetFolderEmpty) {
                    $continuationPrompt += "Target folder '$TargetFolder' in mailbox '$TargetMailbox' is empty.`n"
                }
                $continuationPrompt += 'Do you want to continue with the next batch? (Y/N)'
                $continue = Read-Host -Prompt $continuationPrompt
                if ($continue -eq 'Y') {
                    $continue = $true
                }
                elseif ($continue -eq 'N') {
                    $continue = $true
                    Write-Verbose -Message "Script execution aborted by user."
                    return
                }
                else {
                    $continue = $false
                }
            }
        }
    }
    End {
        if($LogEnabled) {
            Write-Verbose -Message "Log file was created: $LogFilePath"
        }
    }
}

function Disconnect-EOATExchangeWebService {
    <#
    .SYNOPSIS
    Disconnect from Exchange Online EWS. This function clears the script variables.
     
    .DESCRIPTION
    Disconnect from Exchange Online EWS. This function clears the script variables.
 
    .EXAMPLE
    Disconnect-EOATExchangeWebService
 
    This example disconnects from Exchange Online EWS, by clearing the script variables.
 
    #>

    [CmdletBinding()]
    param()

    # Clear script variables
    $script:EwsService = $null
    $script:SourceMailbox = $null

    # Verbose output
    Write-Verbose -Message "Successfully disconnected from Exchange Online EWS"
}

function Get-EOATMailFolder {
    <#
    .SYNOPSIS
    Get a list of mail folders from a mailbox.
     
    .DESCRIPTION
    Get a list of mail folders from a mailbox.
    This function returns a list of mail folders from a mailbox using the Exchange Web Services (EWS) API.
    You can use the returned list of mail folders to select the folders, which you want to use for further steps.
     
    .PARAMETER SearchBase
    String value of the WellKnownFolderName enum. Default value is 'ArchiveMsgFolderRoot'.
    This parameter defines the root folder, from which the function starts to search for mail folders.
     
    .PARAMETER FolderViewCount
    Integer value. Default value is 100.
    This parameter defines the number of mail folders, which are returned by the function.
     
    .PARAMETER ShowGui
    Switch parameter. Default value is $false.
    If this switch is set, the function returns a list of mail folders in a GUI window. You can select the folders, which you want to use for further steps.
     
    .PARAMETER Service
    ExchangeService object. Default value is the script variable $script:EwsService.
    This parameter defines the ExchangeService object, which is used to connect to Exchange Online EWS.
    This parameter will be set automatically, if you use the function 'Connect-EOATExchangeWebService' to connect to Exchange Online EWS.
     
    .EXAMPLE
    Get-EOATMailFolder -SearchBase 'ArchiveMsgFolderRoot' -ShowGui
 
    This example returns a list of mail folders from the root folder 'ArchiveMsgFolderRoot' in a GUI window. You can select the folders, which you want to use for further steps.
 
    .EXAMPLE
    Get-EOATMailFolder -SearchBase 'ArchiveMsgFolderRoot' -FolderViewCount 150
 
    This example returns a list of mail folders from the root folder 'ArchiveMsgFolderRoot'. The number of returned mail folders is 150.
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.Generic.List[Object]])]
    param (
        [ArgumentCompleter({
                [Microsoft.Exchange.Webservices.Data.WellKnownFolderName] | Get-Member -Static -MemberType Properties | Select-Object -ExpandProperty Name
            })]
        [String]
        $SearchBase = 'ArchiveMsgFolderRoot',

        [int]
        $FolderViewCount = 100,

        [switch]
        $ShowGui,

        [Parameter(DontShow)]
        [Microsoft.Exchange.WebServices.Data.ExchangeServiceBase]
        $Service = $script:EwsService
    )

    Process {
        $folderview = New-Object Microsoft.Exchange.WebServices.Data.FolderView($FolderViewCount)
        $folderview.PropertySet = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.Webservices.Data.BasePropertySet]::FirstClassProperties)
        $folderview.PropertySet.Add([Microsoft.Exchange.Webservices.Data.FolderSchema]::DisplayName)
        $folderview.Traversal = [Microsoft.Exchange.Webservices.Data.FolderTraversal]::Deep
        $foldersResult = $Service.FindFolders([Microsoft.Exchange.Webservices.Data.WellKnownFolderName]::$SearchBase, $folderview)
 
        #List folders result
        $foundFolders = $foldersResult | Select-Object -Property DisplayName, TotalCount, UnreadCount, FolderClass, Id | Sort-Object -Property DisplayName

        # if ShowGui is set, show the folders in a GUI window...
        if ($ShowGui) {
            $foundFolders = $foundFolders | Out-GridView -Title 'Select the folders, which you want to use for further steps' -PassThru
        }
        # ...and check if user selected a folder
        if ($null -eq $foundFolders) {
            Write-Verbose -Message "No folder was selected. Please select at least one folder."
            return
        }

        # Check if only one folder was selected. If so, return it as an array
        if ($foundFolders.count -eq 1) {
            $foundFolders = @($foundFolders)
        }

        # return the selected folders as array
        $foundFolders
    }
}

function Get-EOATMailItem {
    <#
    .SYNOPSIS
    Get a list of mail items from a mail folder.
     
    .DESCRIPTION
    Get a list of mail items from a mail folder.
    This function returns a list of mail items from a mail folder using the Exchange Web Services (EWS) API.
    The items are automatically returned from new to old by the Exchange Web Service.
    You can use the returned list of mail items to select the items, which you want to use for further steps.
     
    .PARAMETER MailFolders
    PSCustomObject array. Default value is $null.
    This parameter defines the mail folders, from which the function gets the mail items.
    You can use the function 'Get-EOATMailFolder' to get a list of mail folders.
     
    .PARAMETER ResultSizePerFolder
    Integer value. Default value is null.
    This parameter defines the number of mail items, which are returned by the function per provided folder.
    So, if you provide three mail folders, the function returns 3000000 mail items.
    If you provide three mail folders and set the ResultSizePerFolder to 100, the function returns 300 mail items.
    If you do not provide a value for this parameter, the function returns all mail items per folder.
     
    .PARAMETER StartDate
    DateTime value. Default value is $null.
    This parameter defines the start date of the mail items, which are returned by the function.
    The start date is defined as the "received date" of the mail item.
    The function will search for mails in the provided folder(s) based on the provided ResultSizePerFolder and start date.
 
    .PARAMETER EndDate
    DateTime value. Default value is $null.
    This parameter defines the end date of the mail items, which are returned by the function.
    The end date is defined as the "received date" of the mail item.
    The function will search for mails in the provided folder(s) based on the provided ResultSizePerFolder and end date.
 
    .PARAMETER Service
    ExchangeService object. Default value is the script variable $script:EwsService.
    This parameter defines the ExchangeService object, which is used to connect to Exchange Online EWS.
    This parameter will be set automatically, if you use the function 'Connect-EOATExchangeWebService' to connect to Exchange Online EWS.
     
    .EXAMPLE
    Get-EOATMailItem -MailFolders $MailFolders
 
    This example returns a list of mail items from the mail folders defined in the variable $MailFolders.
 
    .EXAMPLE
    Get-EOATMailItem -MailFolders $MailFolders
 
    This example returns a list of mail items from the mail folders defined in the variable $MailFolders. The number of returned mail items is 1000000.
 
    .EXAMPLE
    Get-EOATMailFolder -SearchBase ArchiveMsgFolderRoot -ShowGui | Get-EOATMailItem -ResultSize 150
 
    This example returns a list of mail items from the mail folders selected in the GUI window. The number of returned mail items is 150 per provided folder.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    [OutputType([System.Collections.Generic.List[Object]])]
    param (

        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Default')]
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'DateRange')]
        [PSCustomObject[]]
        $MailFolders,

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'DateRange')]
        [Int32]
        $ResultSizePerFolder,

        [Parameter(Mandatory = $true, ParameterSetName = 'DateRange')]
        [datetime]
        $StartDate,

        [Parameter(Mandatory = $true, ParameterSetName = 'DateRange')]
        [datetime]
        $EndDate,

        [Parameter(ParameterSetName = 'Default', DontShow)]
        [Parameter(ParameterSetName = 'DateRange', DontShow)]
        [Microsoft.Exchange.WebServices.Data.ExchangeServiceBase]
        $Service = $script:EwsService
    )

    Begin {

        # Check if StartDate and EndDate are set and if StartDate is before EndDate
        if ($StartDate -and $EndDate -and $StartDate -gt $EndDate) {
            Write-Error -Message "The StartDate must be before the EndDate."
            return
        }

        # Check if input object for Mailfolders was provided. If not, ignore for now
        if ($null -eq $MailFolders) {
            return
        }

        # Check if input object contains property 'Id'
        if (($MailFolders | Get-Member -MemberType NoteProperty).Name -notcontains "Id") {
            Write-Error -Message "The input object must contain a property named 'Id'. Please use the function 'Get-EOATMailFolder' to retrieve the folders."
            return
        }
    }

    Process {
        # Prepare return value
        $returnValue = [System.Collections.Generic.List[Object]]::new()

        # List mail items in selected folders
        foreach ($mailFolder in $MailFolders) {
            # prepare variable to count items in this folder
            $returnedItemsInThisFolder = 0

            # Define SubFolderId
            $SubFolderId = New-Object -TypeName Microsoft.Exchange.WebServices.Data.FolderId($mailFolder.id)
            
            #Define ItemView to retrieve items in pages
            $pageSize = 1000
            $offSet = 0
            $ivItemView = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ItemView(($pageSize + 1), $offSet)
            $ivItemView.PropertySet = New-Object -TypeName Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties, [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::DateTimeReceived)
            $ivItemView.OrderBy.Add([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::DateTimeReceived, 'Descending')

            # Prepare Searchfilter, if StartDate and EndDate were provided
            if ($StartDate -and $EndDate) {
                $searchQuery = "received:>=$($StartDate.ToString("MM/dd/yyyy")) AND received:<=$($EndDate.ToString("MM/dd/yyyy"))"
            }

            # Get items from folder and add them to list
            $continueLoop = $true
            do {
                if ($StartDate -and $EndDate) {
                    $foundItems = $Service.FindItems($SubFolderId, $searchQuery, $ivItemView)
                }
                else {
                    $foundItems = $Service.FindItems($SubFolderId, $ivItemView)
                }

                # Check if more items are available
                $continueLoop = $foundItems.MoreAvailable
                if ($foundItems.MoreAvailable) {
                    $ivItemView.Offset += $pageSize
                }

                foreach ($foundItem in $foundItems) {
                    # Add item to list
                    $returnValue.Add($foundItem)
                    $returnedItemsInThisFolder++
                    
                    if($returnedItemsInThisFolder -eq $ResultSizePerFolder) {
                        Write-Verbose -Message "ResultSize of $ResultSizePerFolder items reached. Ending search."
                        
                        # end outer do-while loop
                        $continueLoop = $false
                        
                        # end inner foreach loop
                        break
                    }
                }

                Write-Verbose -Message "Items Count: $($foundItems.Items.Count), Offset: $($ivItemView.Offset)"

            } while ($continueLoop)
        }

        # return the selected items as list
        $returnValue

    }
}

function Move-EOATMailItemToOtherMailbox {
    <#
    .SYNOPSIS
    Move mail items to another mailbox
     
    .DESCRIPTION
    Move mail items to another mailbox
    This function moves mail items to another mailbox using the Exchange Web Services (EWS) API.
     
    .PARAMETER MailItems
    PSCustomObject array. Default value is $null.
    This parameter defines the mail items, which should be moved.
    You can use the function 'Get-EOATMailItem' to get a list of mail items.
     
    .PARAMETER TargetMailbox
    String value. Default value is $null.
    This parameter defines the target mailbox, to which the mail items should be moved.
     
    .PARAMETER TargetFolder
    String value. Default value is $null.
    This parameter defines the target folder, to which the mail items should be moved.
     
    .PARAMETER MaxBatchSize
    Integer value. Default value is 96636764160 (90 GB).
    This parameter defines the maximum batch size in Byte, which is used to retrieve the mail items.
    If more than 90 GB of mail items are send to this function, the mails will be split into batches minimum 90 GB.
    A single batch could be larger than 90GB, because we add items to the batch until AT LEAST 90GB are reached.
 
    .PARAMETER WaitTime
    Integer value. Default value is 300 (5 minutes).
    This parameter defines the time in seconds, which the function will wait, before continuing with the next batch.
 
    .PARAMETER CheckTargetFolderEmpty
    Switch value. Default value is $true.
    This parameter defines, if the function should check, if the target folder is empty, before continuing with the next batch.
    If the target folder is not empty, the function will wait $WaitTime seconds before continuing with the next batch.
    As soon as the target folder is empty, the function will ask the script user, if the function should continue with the next batch.
 
    .PARAMETER LogEnabled
    Switch value. Default value is $false.
    This parameter defines, if the function should log the mail item copy process to a CSV file.
 
    .PARAMETER LogFilePath
    String value. Default value is "$env:temp\Copy-EOATMailItemToOtherMailbox-$(Get-Date -Format yyyyMMddhhmmss).csv".
    This parameter defines the path to the log file, which is used to log the mail item copy process.
    The log file have the following header: SourceMailbox, SourceFolderId, TargetMailbox, TargetFolder, TargetFolderId, SourceMailItemId, Sender, Subject, Received, SizeInMB, CurrentWindowsUser
 
    .PARAMETER LogDelimiter
    String value. Default value is ';'.
    This parameter defines the delimiter, which is used to separate the values in the log file.
 
    .PARAMETER Service
    ExchangeService object. Default value is the script variable $script:EwsService.
    This parameter defines the ExchangeService object, which is used to connect to Exchange Online EWS.
    This parameter will be set automatically, if you use the function 'Connect-EOATExchangeWebService' to connect to Exchange Online EWS.
     
    .PARAMETER Confirm
    Switch value. Default value is $true.
    This parameter defines, if the function should ask the script user to continue with the next batch, if the target folder is empty.
 
    .PARAMETER WhatIf
    Switch value. Default value is $false.
    This parameter defines, if the function should simulate the execution of the function.
    This parameter is currently not implemented
 
    .EXAMPLE
    Move-EOATMailItemToOtherMailbox -MailItems $MailItems -TargetMailbox 'temporary_mailbox@contoso.com' -TargetFolder 'Inbox'
     
    This example moves the mail items defined in the variable $MailItems to the mailbox 'temporary_mailbox@contoso.com'. The mail items will be moved to the folder 'Inbox'.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject[]]
        $MailItems,

        [Parameter(Mandatory = $true)]
        [String]
        $TargetMailbox,

        [ArgumentCompleter({
                [Microsoft.Exchange.Webservices.Data.WellKnownFolderName] | Get-Member -Static -MemberType Properties | Select-Object -ExpandProperty Name
            })]
        [Parameter(Mandatory = $true)]
        [String]
        $TargetFolder,

        [Int64]
        $MaxBatchSize = 90GB,

        [Int32]
        $WaitTime = 300,

        [bool]
        $CheckTargetFolderEmpty = $true,

        [switch]
        $LogEnabled,

        [string]
        $LogFilePath = "$env:temp\Move-EOATMailItemToOtherMailbox-$(Get-Date -Format yyyyMMddhhmmss).csv",

        [string]
        $LogDelimiter = ';',

        [Parameter(DontShow)]
        [Microsoft.Exchange.WebServices.Data.ExchangeServiceBase]
        $Service = $script:EwsService
    )

    Begin {
        # trap statement
        $ErrorActionPreference = 'Stop'
        trap {
            Write-Error -Message $_.Exception.Message
            Write-Error -Message $_.Exception.StackTrace
            return
        }

        # Check if target folder exists
        try {
            $targetFolderId = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service, [Microsoft.Exchange.WebServices.Data.FolderId]::new($TargetFolder, $TargetMailbox))
        }
        catch {
            Write-Error -Message "The target folder '$TargetFolder' does not exist in mailbox '$TargetMailbox' or you do not have FullAccess permission on the target mailbox."
            return
        }

        # Check, if we have to split the result array
        if (($MailItems.size | Measure-Object -Sum).Sum -gt $MaxBatchSize) {
            $mailItemBatches = Split-EOATMailItem -MailItems $MailItems -MaxBatchSize $MaxBatchSize
        }
        else {
            $mailItemBatches = [System.Collections.Generic.List[PSCustomObject]]::new()
            $null = $mailItemBatches.Add($MailItems)
        }

        # If LogEnabled, check if the log file already exists. If so, stop the script and ask the user to delete the log file.
        if($LogEnabled) {
            if (Test-Path -Path $LogFilePath) {
                Write-Error -Message "The file '$LogFilePath' already exists. Please delete the log file and try again."
                return
            }
        }
    }

    # Move mail items
    Process {

        foreach ($mailItemBatch in $mailItemBatches) {
            Write-Verbose -Message "Moving $($mailItemBatch.Count) mail items to folder '$TargetFolder' in mailbox '$TargetMailbox'"
            foreach ($mailItem in $mailItemBatch) {
                # Write progress
                $progressProps = @{
                    Activity        = "Batch $($mailItemBatches.IndexOf($mailItemBatch) + 1) of $($mailItemBatches.count)"
                    Status          = "Moving mail item '$($mailItem.Subject)' to folder '$TargetFolder' in mailbox '$TargetMailbox'"
                    PercentComplete = (($mailItemBatch.IndexOf($mailItem) + 1) / $mailItemBatch.Count * 100)
                }
                Write-Progress @progressProps
                
                $retryCount = 0
                $moveSuccess = $false
                do {
                    # If the method fails three times with the error "The server cannot service this request right now. Try again later.", the function will return an error
                    if($retryCount -eq 3) {
                        Write-Error -Message "The method failed with error 'Try again later' three times. The function will return an error."
                        return
                    }

                    try {
                        # Move mail item
                        $null = $mailItem.Move($targetFolderId.Id)
                    }
                    catch {
                        if ($_.Exception.Message -like "*The server cannot service this request right now. Try again later.*") { # If the error message contains "The server cannot service this request right now. Try again later.", we will wait and retry the method
                            $retryCount++
                            $backOffMilliseconds = $_.Exception.InnerException.BackOffMilliseconds + 100
                            Write-Warning -Message "The method failed with error '$($_.Exception.Message)'. Waiting $backOffMilliseconds milliseconds before retrying the method."
                            Start-Sleep -Milliseconds $backOffMilliseconds
                            continue
                        }
                        elseif ($_.Exception.Message -like "*Unauthorized*") {
                            $retryCount++
                            Write-Warning -Message "The method failed with error '$($_.Exception.Message)'. Refreshing token and retrying the method."
                            # Refresh token
                            $script:EwsToken.RenewToken()

                            # Reconnect to EWS
                            $script:EwsService.Credentials = [Microsoft.Exchange.WebServices.Data.OAuthCredentials]$script:EwsToken.AccessToken
                            Write-Verbose "Starting Autodiscover for $MailboxName"
                            $script:EwsService.AutodiscoverUrl($MailboxName, { $true }) # use overload with callback
                            $Service = $script:EwsService

                            continue
                        }
                        else { # If the error message does not contain "The server cannot service this request right now. Try again later.", we will write the error message and stack trace to the console and return
                            throw $_
                        }
                    }

                    # If the method was successful, we will set $moveSuccess to $true
                    $moveSuccess = $true
                } while (-not $moveSuccess)

                if ($LogEnabled) {
                    # Create log object
                    $logObject = @{
                        SourceMailbox      = $script:SourceMailbox
                        # SourceFolder = '' # SourceFolder Name is not available on this object. Will be ignored for now for performance reasons. SourceFolderId can be used to get the SourceFolder Name using Get-EOATMailFolder.
                        SourceFolderId     = $mailItem.ParentFolderId.UniqueId
                        TargetMailbox      = $TargetMailbox
                        TargetFolder       = $TargetFolder
                        TargetFolderId     = $targetFolderId.Id
                        SourceMailItemId   = $mailItem.Id.UniqueId
                        Sender             = $mailItem.From.Address
                        Subject            = ''
                        Received           = $mailItem.DateTimeReceived.ToString('yyyy-MM-dd-hh-mm-ss')
                        SizeInMB           = "{0:N2}" -f ($mailItem.Size / 1024 / 1024)
                        CurrentWindowsUser = "$env:USERDOMAIN\$env:USERNAME"
                    }

                    # If the mail item has a subject, we will add the subject to the log object
                    if($null -ne $mailItem.Subject -and "" -ne $mailItem.Subject) {
                        $logObject.Subject = $mailItem.Subject
                    }

                    # Export log object to CSV file
                    $logObject | Export-Csv -Path $LogFilePath -Append -NoTypeInformation -Encoding utf8 -Delimiter $LogDelimiter
                }
            }

            # if we had only one batch or if we are at the last batch, we will not ask the user to continue with the next batch
            if (($mailItemBatches.count -eq 1) -or ($mailItemBatches.IndexOf($mailItemBatch) -eq ($mailItemBatches.count - 1))) {
                Write-Verbose -Message "No more batches to process."
                return
            }
                
            # Check the target folder count every x seconds before continuing with the next batch, to prevent the mailbox from exceeding the quota
            # Ask the script user if we should continue, as soon as the target folder is empty
            if($CheckTargetFolderEmpty) {
                $targetFolderItemcount = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service, [Microsoft.Exchange.WebServices.Data.FolderId]::new($TargetFolder, $TargetMailbox)) | Select-Object -ExpandProperty TotalCount
                while ($targetFolderItemcount -ne 0) {
                    Write-Warning -Message "Target folder '$TargetFolder' in mailbox '$TargetMailbox' is not empty. Waiting $WaitTime seconds before continuing with the next batch."
                    Start-Sleep -Seconds $WaitTime
                    $targetFolderItemcount = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service, [Microsoft.Exchange.WebServices.Data.FolderId]::new($TargetFolder, $TargetMailbox)) | Select-Object -ExpandProperty TotalCount
                }
            }

            # If confirm is set to false, we will continue with the next batch immediately
            if ($ConfirmPreference -eq "None") {
                continue
            }
        
            # Ask the script user if we should continue, if the target folder is empty
            $continue = $false
            while ($continue -eq $false) {
                $continuationPrompt = ""
                if($CheckTargetFolderEmpty) {
                    $continuationPrompt += "Target folder '$TargetFolder' in mailbox '$TargetMailbox' is empty.`n"
                }
                $continuationPrompt += 'Do you want to continue with the next batch? (Y/N)'
                $continue = Read-Host -Prompt $continuationPrompt
                if ($continue -eq 'Y') {
                    $continue = $true
                }
                elseif ($continue -eq 'N') {
                    $continue = $true
                    Write-Verbose -Message "Script execution aborted by user."
                    return
                }
                else {
                    $continue = $false
                }
            }
        }
    }
    End {
        if($LogEnabled) {
            Write-Verbose -Message "Log file was created: $LogFilePath"
        }
    }
}