EXOTools.psm1

#HashTable for RemotePowerShell Endpoints
$ConnectionURI = @{
    "ExchangeOnline"   = "https://outlook.office365.com/powershell-liveid"
    "ComplianceCenter" = "https://ps.compliance.protection.outlook.com/powershell-liveid"
    "OnPrem"           = ""
}
$sessionOpts = New-PSSessionOption -IdleTimeout 3600000 -OpenTimeout 0
$delim = "=" * 80

Function Open-XMLFiles () {
    <#
 .Synopsis
  Imports all XML files in a specified directory into variables based on the file name.
 
 .Description
  Imports all XML files in a specified directory into variables based on the file name. Invalid characters are automatically stripped from the file name so that the variable names are valid
 
 .Parameter FilePath
  The directory where the XML files are located.
 
 .Example
    Open-XMLFiles
    Prompt for the directory where the XML files are located
 
 .Example
    Open-XMLFiles -FilePath C:\Temp\Data
    Load XML files is the provided directory
 
 .Example
    Open-XMLFiles -FilePath C:\Temp\Data -Verbose
    Load XML files is the provided directory and show the variables
    #>

    Param (
        [Parameter(Mandatory = $false, Position = 0)][string]$FilePath, 
        [Parameter(Mandatory = $false)][switch]$ShowVariables = $false
    )
    


    $files = Get-ChildItem -Filter *.xml -Path $FilePath
    foreach ($file in $files) {
        #Remove special characters from the filename
        $Vname = $file.BaseName -replace '[#?\{_-]', ''
        $Vname = $Vname.replace(".", "")
        if (get-variable $vname -ErrorAction SilentlyContinue) {
            #Set Variable instead of creating a new one
            Set-Variable $Vname -Value (Import-Clixml $file.FullName) -Scope Global
            Write-Verbose "Imported $($file.BaseName) into variable `$$($vname)"
            Write-Verbose "Imported $($file.BaseName) into variable `$$($vname)"
        }
        Else {
            new-variable $vname -Value (Import-Clixml $file.FullName) -Scope Global
            Write-Verbose "Imported $($file.BaseName) into variable `$$($vname)"
        }
    }
}
function _readFolderBrowserDialog() {
    #Internal Function
    Param (
        [Parameter(Mandatory = $false)][string]$Message, 
        [Parameter(Mandatory = $false)][string]$InitialDirectory, 
        [Parameter(Mandatory = $false)][switch]$NoNewFolderButton = $false
    )
    $browseForFolderOptions = 0     
    if ($NoNewFolderButton) { 
        $browseForFolderOptions += 512 
    }       
    $app = New-Object -ComObject Shell.Application     
    $folder = $app.BrowseForFolder(0, $Message, $browseForFolderOptions, $InitialDirectory)     
    if ($folder) { 
        $selectedDirectory = $folder.Self.Path 
    } 
    else { 
        $selectedDirectory = '' 
    }     
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($app) > $null    
    return $selectedDirectory
}
Function _getFileName() {
    #Internal Function
    Param (
        [Parameter(Mandatory = $false)][string]$initialDirectory,
        [Parameter(Mandatory = $false)][string]$Filter
    )
    [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null
    $OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
    $OpenFileDialog.initialDirectory = $initialDirectory
    $OpenFileDialog.filter = $Filter
    $OpenFileDialog.ShowDialog() | Out-Null
    $OpenFileDialog.filename
} 
Function Write-Header() {
<#
 .Description
Outputs a header with either lines above and below or just below
 .Parameter Text
 The text to display
 .Parameter Single
 Only output a single line below the text
 .Parameter LogFile
 File to use to export the text
 .Parameter SkipLogging
 Do not log the header
 .Example
 Write-Header -Text "Example Header"
================================================================================
Example Header
================================================================================
.Example
 Write-Header -Text "Example Header" -Single
Example Header
================================================================================
#>

    Param (
        [Parameter(Mandatory = $false)][string]$Text,
        [Parameter(Mandatory = $false)][switch]$Single,
        [Parameter(Mandatory = $false)][string]$LogFile,
        [Parameter(Mandatory = $false)][switch]$SkipLogging = $false
    )
    if (!$single) {Write-host $delim}
    Write-Host $text
    Write-Host $Delim

    if (!$SkipLogging -and $LogFile) {
        if (!$single) {Write-Log -Text $Delim -LogFile $LogFile}
        Write-Log -Text $text  -LogFile $LogFile
        Write-Log -Text $Delim -LogFile $LogFile
    }

}
Function Write-Log {
<#
 .Description
Custom logging function
 .Parameter Text
 The text to display
 .Parameter AddDateTime
 Prepend a timestamp to the beginning of the text
 .Parameter OutputToConsole
 Display the text in the PowerShell console. Default is to only log to file
 .Parameter LogFile
 File to use to export the text
 .Parameter Skip
 Skip logging the text to file - Used to output to the console only
  
 .Example
 Write-Header -Text "Example Header"
================================================================================
Example Header
================================================================================
.Example
 Write-Header -Text "Example Header" -Single
Example Header
================================================================================
#>


    Param (
        [Parameter(Mandatory = $true)][string]$Text,
        [Parameter(Mandatory = $false)][Switch]$AddDateTime = $False, 
        [Parameter(Mandatory = $false)][switch]$OutputToConsole = $false, 
        [Parameter(Mandatory = $false)][string]$LogFile, 
        [Parameter(Mandatory = $false)]$Skip = $false
    )
    if ($OutputToConsole) {$Text}
    if (!$Skip) {
        # Get the current date
        if ($AddDateTime) {$beg = "[$(Get-Date -Format G)] - " }
        
        # Write everything to our log file
        ( $beg + $Text) | Out-File -FilePath $LogFile -Append
    }
}

function Get-CachedCredential () {
<#
 .Synopsis
  Wrapper command to Retrieve and store credentials in Credential Manager
 .Description
 Retrieves stored credentials from Credential Manager. Useful for admin credentials for remote powershell (Exchange Online, Azure AD, etc)
 Requires the module CredentialManager - Install-Module CredentialManager
 .Parameter Target
  Name of the credentials to retrieve/store in Credential Manager
 .Parameter Delete
 Delete the credentials and prompt for new credentials
 .Parameter LogFile
 File to use to export the text
 .Example
 Get-CachedCredential -Target ExchangeOnline
Retrieves the credentials for the target "Exchange Online".
If the credentials don't exist, the system will generate a credential prompt and then subsequently store the credentials
.Example
 Get-CachedCredential -Target ExchangeOnline -Delete
Retrieves the credentials for the target "Exchange Online".
If the credentials don't exist, the system will generate a credential prompt and then subsequently store the credentials
If the credentials do exist, they will be deleted and the system will generate a credential prompt and then subsequently store the credentials
#>

    Param (
        [Parameter(Mandatory = $true)][string]$Target,
        [Parameter(Mandatory = $false)][switch]$Delete = $false
    )
    #Check for creds, if they don't exist, prompt and store
    if ($Delete) {Remove-StoredCredential -Target $Target|out-null}
    $cachedCred = Get-StoredCredential -Target $Target -ErrorAction SilentlyContinue
    if ($cachedCred) {
        return $cachedCred
    }
    else {
        $cred = get-credential
        $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($cred.password)
        $PlainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
        New-StoredCredential -Target $Target -UserName $cred.UserName -Password $PlainPassword -Type GENERIC -Persist LOCALMACHINE|out-null
    }
    return $cred
}

Function Start-Analysis () {
    <#
 .Synopsis
 Imports, analyzes data gathering scripts and exports to a usuable format
 
 .Description
 Data Gathering scripts are located in the Module folder and can be opened with List-DataGatheringFiles
 
 .Parameter Type
 The type of analysis to run
 
 .Parameter FilePath
 Directory where the source files exist
 
 .Example
 Start-Analysis -Type RetentionPolicy
 Run the data through the Retention Policy algorithm
 
 .Example
 Start-Analysis -Type OfficeAddins
 Run the data through the Office Addins algorithm
 
 .Example
 Start-Analysis -Type FreeBusy
 Run the data through the Free/Busy algorithm
 
 .Example
 Start-Analysis -Type Migration
 Run the data through the Migration algorithm
 
 .Example
 Start-Analysis -Type FocusedInbox
 Run the data through the Focused Inbox algorithm
 
 .Example
 Start-Analysis -Type MailFlow
 Run the data through the Mail Flow algorithm
#>

    Param (
        [Parameter(Mandatory = $true)][ValidateSet("RetentionPolicy", "OfficeAddIns", "FreeBusy", "Migration", "FocusedInbox", "MailFlow")][string]$Type,
        [string]$FilePath = (_readFolderBrowserDialog),
        [Switch]$Live
    )
    switch ($Type) {
        "RetentionPolicy" { _AnalyzeRetentionPolicyData -FilePath $FilePath -Live:$Live}
        "OfficeAddIns" { _AnalyzeOfficeAddinsData -FilePath $FilePath -Live:$Live}
        "FreeBusy" { _AnalyzeFreeBusyData -FilePath $FilePath}
        "Migration" { _AnalyzeMigrationData -FilePath $FilePath -Live:$Live}
        "FocusedInbox" { _AnalyzeFocusedInbox -FilePath $FilePath}
        "MailFlow" { _AnalyzeMailFlow -FilePath $FilePath}
    }

}
Function _AnalyzeRetentionPolicyData () {
    Param (
        [string]$FilePath,
        [switch]$Live
    )
    if ($Live) {
        [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic') | Out-Null
        $CloudMailbox = [Microsoft.VisualBasic.Interaction]::InputBox("Enter the Cloud Mailbox's UserPrincipalName in the format user@domain.com", "CloudMailbox")
        $MRMRetentionPoliciesAll = Get-RetentionPolicy
        $MRMRetentionPolicyTagsAll = Get-RetentionPolicyTag
        $GetMailbox = Get-Mailbox $CloudMailbox
        $mbxfolderstatsPrimary = Get-MailboxFolderStatistics $CloudMailbox -IncludeOldestAndNewestItems| export-clixml mbxfolderstats-Primary.xml
        if ($GetMailbox.ArchiveGUID -ne "00000000-0000-0000-0000-000000000000") {$mbxfolderstatsArchive = Get-MailboxFolderStatistics $CloudMailbox -Archive}
        $diaglogsmrm = Export-MailboxDiagnosticLogs -ComponentName MRM $CloudMailbox
        $diaglogsextend = Export-MailboxDiagnosticLogs -ExtendedProperties $CloudMailbox
        #$Orgconfig = Get-Organizationconfig | Select-Object is*,*plan*,*stat* -erroraction silentlycontinue
    }
    Else {
        Open-XMLFiles -FilePath $FilePath
    }
    $LogFile = Join-Path -Path $FilePath -ChildPath "RetentionPolicyAnalysis.txt"
    $findings = Join-Path -Path $FilePath -ChildPath "RetentionPolicyAnalysis-Findings.txt"
    
    #Show List of RP
    Write-Header "All Retention Policies" -LogFile $LogFile
    Write-Log -Text($MRMRetentionPoliciesAll| Format-List Name, IsDefault, RetentionPolicyTagLinks|out-string) -OutputToConsole -LogFile $LogFile

    $zeroTags = $MRMRetentionPoliciesAll | Where-Object {$_.RetentionPolicyTagLinks.count -eq 0}
    if ($zeroTags) {
        Write-Header -text "The following retention policies have no associated tags" -LogFile $LogFile
        Write-Log -Text ($zeroTags|Select-Object Name|out-string) -OutputToConsole -LogFile $LogFile
    }
    Write-Header "All Retention Policy Tags" -LogFile $LogFile
    Write-Log -Text($MRMRetentionPolicyTagsAll|Select-Object Name, Type, MessageClassDisplayName, RetentionAction, AgeLimitForRetention, RetentionEnabled| Sort-Object  Type, Name| Format-Table |out-string) -OutputToConsole -LogFile $LogFile

    Write-Header "Retention Policy Tags that apply to the specific mailbox: $($GetMailbox.RetentionPolicy)" -LogFile $LogFile
    $rpt = foreach ($rpt in ($MRMRetentionPoliciesAll| Where-Object {$_.identity -eq $GetMailbox.RetentionPolicy}).RetentionPolicyTagLinks) {$MRMRetentionPolicyTagsAll | Where-Object {$_.identity -eq $rpt}}
    If ($rpt) {
        Write-Log -Text($rpt|Select-Object Name, Type, MessageClassDisplayName, RetentionAction, AgeLimitForRetention, RetentionEnabled| Sort-Object  Type, Name| Format-Table |out-string) -OutputToConsole -LogFile $LogFile
    }
    else {
        Write-Log -Text "No associated tags" -OutputToConsole -LogFile $LogFile
    }

    if (!($rpt|Where-Object {$_.Type -eq "RecoverableItems"})) {
        Write-Header "WARNING: Mailbox's Retention Policy does not have a Recoverable Items tag" -LogFile $findings
        Write-Log ("Please add one of the following tags to $($GetMailbox.RetentionPolicy)"| Out-String) -OutputToConsole -LogFile $findings
        Write-Log -Text($MRMRetentionPolicyTagsAll|Where-Object {$_.Type -eq "RecoverableItems"}|Select-Object Name, Type, MessageClassDisplayName, RetentionAction, AgeLimitForRetention, RetentionEnabled| Sort-Object  Type, Name| Format-Table |out-string) -OutputToConsole -LogFile $findings
    }

    #check for InPlaceHolds, Litigation Hold and Retention Holds

    if ($GetMailbox.LitigationHoldEnabled -eq $true) {
        Write-Header "WARNING: Litigation Hold is enabled" -LogFile $findings
        Write-Log ($GetMailbox|Format-List Litigation*|Out-string) -OutputToConsole -LogFile $findings
    }

    if ($getmailbox.inplaceholds) {
        Write-Header "WARNING: InPlace Holds are present" -LogFile $findings
        Write-Log ($GetMailbox.InplaceHolds| out-string) -OutputToConsole -LogFile $findings
    }

    if ($GetMailbox.RetentionHoldEnabled -eq $true) {
        #Retention Hold is enabled - Items will be Tagged but no actions will be taked on the tags
        Write-Header "WARNING: Retention Hold is enabled" -LogFile $findings
        Write-log "This mailbox has retention hold enabled. To fix this run the command below from Exchange Online PowerShell" -OutputToConsole -LogFile $findings
        Write-Log "Set-Mailbox -Identity $($GetMailbox.ExchangeGUID.Guid) -RetentionHoldEnabled `$false" -OutputToConsole -LogFile $findings
    }

    if ($GetMailbox.ElcProcessingDisabled -eq $true) {
        Write-Header "WARNING: ELC Processing is disabled" -LogFile $findings
        Write-log "This mailbox has ELC Processing disabled. The Managed Folder Assistant will not be able to process this mailbox. To fix this run the command below from Exchange Online PowerShell" -OutputToConsole -LogFile $findings
        Write-Log "Set-Mailbox -Identity $($GetMailbox.ExchangeGUID.Guid) -ELCProcessingDisabled `$false" -OutputToConsole -LogFile $findings
    }

    If ($GetMailbox| Where-Object {$_.MailboxLocations -match "AuxArchive"}) {
        Write-Header "User has Aux Archive Mailboxes - Unlimited Archiving" -LogFile $LogFile
    }
    else {
        Write-Header "User does not have Aux Archive Mailboxes - No Unlimited Archiving" -LogFile $LogFile
    }

    if ($mbxfolderstatsPrimary) {
        $taggedFolders = $mbxfolderstatsPrimary| Where-Object {$_.DeletePolicy -ne $null -or $_.ArchivePolicy -ne $null}
        if ($taggedFolders) {
            Write-Header "Mailbox Folders with Retention Policies applied" -LogFile $LogFile
            Write-Log -Text( $taggedFolders| Format-Table -a Folderpath, *policy*|out-string) -OutputToConsole -LogFile $LogFile
        }
        Write-Header "Mailbox Folder Statistics - Primary" -LogFile $LogFile
        Write-Log -Text($mbxfolderstatsPrimary|Select-Object FolderPath, FolderType, FolderSize, ItemsInFolder | Format-Table -AutoSize|out-string) -OutputToConsole -LogFile $LogFile   

        Write-Header "Mailbox Folder Statistics - Primary - Last Received Date" -LogFile $LogFile
        Write-Log -Text($mbxfolderstatsPrimary | Select-Object FolderPath, OldestItemReceivedDate| Where-Object {$_.OldestItemReceivedDate -ne $null}| Format-Table -AutoSize|out-string)  -OutputToConsole -LogFile $LogFile  

        If (($mbxfolderstatsPrimary  | Where-Object {$_.Name -eq "Recoverable Items"}).FolderAndSubFolderSize -match "100 GB") {
            Write-Header "WARNING: Primary Mailbox's Recoverable Items is full" -LogFile $findings
        }

        Write-Header "Recoverable Item Folder Statistics - Primary" -LogFile $LogFile
        Write-Log -Text($mbxfolderstatsPrimary|Where-Object {$_.TargetQuota -eq "Recoverable"}|Select-Object FolderPath, FolderType, FolderSize, FolderAndSubFolderSize, ItemsinFolder | Format-Table -a|out-string) -OutputToConsole -LogFile $LogFile

    }

    if ($mbxfolderstatsArchive) {
        Write-Header "Mailbox Folder Statistics - Archive" -LogFile $LogFile
        Write-Log -Text($mbxfolderstatsArchive|Select-Object FolderPath, FolderType, FolderSize, ItemsInFolder | Format-Table -AutoSize|out-string) -OutputToConsole -LogFile $LogFile

        If (($mbxfolderstatsArchive  | where-object {$_.Name -eq "Recoverable Items"}).FolderAndSubFolderSize -match "100 GB") {
            Write-Header "WARNING: Archive Mailbox's Recoverable Items is full" -LogFile $findings
        }
        Write-Header "Recoverable Item Folder Statistics - Archive" -LogFile $LogFile
        Write-Log -Text($mbxfolderstatsArchive|Where-Object {$_.TargetQuota -eq "Recoverable"}|Select-Object FolderPath, FolderType, FolderSize, FolderAndSubFolderSize, ItemsinFolder | Format-Table -a|out-string) -OutputToConsole -LogFile $LogFile    
    }
    #Write-Header "Diagnostics"
    Write-Header "Diagnostic Logs - MRM" -LogFile $LogFile
    Write-Log -Text($diaglogsmrm.MailboxLog| Format-List |out-string) -OutputToConsole -LogFile $LogFile
    Write-Header "Diagnostic Logs - Extended Properties" -LogFile $LogFile
    [xml]$xml = $diaglogsextend.MailboxLog
    Write-Log -Text($xml.Properties.MailboxTable.Property| Where-Object {$_.Name -match "ELC"} | Format-Table -a|out-string) -OutputToConsole -LogFile $LogFile

    #open the logfile
    Invoke-Item $LogFile
    #Open the findings file
    Invoke-Item $findings
}
Function _AnalyzeOfficeAddinsData() {
    #Add Parameter Sets (FilePath or Live)?
    Param (
        [string]$FilePath,
        [Switch]$Live
    )

    if ($Live) {
        #Query for data directly
        [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic') | Out-Null
        $CloudMailbox = [Microsoft.VisualBasic.Interaction]::InputBox("Enter the Cloud Mailbox's UserPrincipalName in the format user@domain.com", "CloudMailbox")
        $orgConfig = Get-OrganizationConfig
        $orgapp = get-app -OrganizationApp
        $mbxapp = Get-App -Mailbox $CloudMailbox
        $mbx = Get-Mailbox $CloudMailbox
        $RAP = Get-RoleAssignmentPolicy
    }
    else {
        #Load Data from files
        Open-XMLFiles -FilePath $FilePath
    }
    $LogFile = Join-Path -Path $FilePath -ChildPath "OfficeAddInsAnalysis.txt"
    
    #Office Apps
    Write-header "Mailbox Information" -LogFile $LogFile
    Write-Log -Text($mbx|Select-Object ExchangeGUID, RoleAssignmentPolicy |out-string) -OutputToConsole -LogFile $LogFile
    Write-Header "Role Assignment Policies" -LogFile $LogFile
    Write-Log -Text($rap| Format-List Name, Description, AssignedRoles, WhenCreatedUTC, WhenChangedUTC |out-string) -OutputToConsole -LogFile $LogFile


    Write-Header "Organization Configuration" -LogFile $LogFile
    Write-Log -Text($orgconfig|Select-Object Name, AppsForOfficeEnabled, AdminDisplayVersion, RBACConfigurationVersion, WhenChangedUTC |out-string) -OutputToConsole -LogFile $LogFile
    if ($orgconfig.AppsForOfficeEnabled -eq $false) {
        Write-Warning "Apps for Office has been disabled at the Organization LeveL"
    }
    Write-Header "Organization Apps" -LogFile $LogFile
    Write-Log -Text($orgapp| Format-List DisplayName, Description, Enabled, ProvidedTo, Requirements |out-string) -OutputToConsole -LogFile $LogFile
    Write-Header "Mailbox Installed Apps" -LogFile $LogFile
    Write-Log -Text($mbxapp| Format-List DisplayName, Description, ProvidedTo, Requirements |out-string) -OutputToConsole -LogFile $LogFile


    Write-Header ("Assignment Role Check - {0}" -f $mbx.RoleAssignmentPolicy) -LogFile $LogFile
    $Roles = $RAP | Where-Object {$_.Name -eq $mbx.RoleAssignmentPolicy}|Select-Object -ExpandProperty AssignedRoles
    $roleset = "My Custom Apps", "My Marketplace Apps", "My ReadWriteMailbox Apps"

    foreach ($role in $roleset) {
        if ($roles| Select-string -Pattern $role) {
            Write-Log -Text( "$($role) role assigned") -OutputToConsole -LogFile $logfile
        }
        else {
            Write-Warning "$role role not in the designated user's Role Assignment Policy"
            Write-Log "WARNING: $role role not in the designated user's Role Assignment Policy" -LogFile $logfile
        }
    }
}
Function _AnalyzeFreeBusyData () {
    Param (
        [string]$FilePath
    )
    if (!$FilePath) {
        $FilePath = _readFolderBrowserDialog
    }
    Open-XMLFiles -FilePath $FilePath
    $LogFile = Join-Path -Path $FilePath -ChildPath "FreeBusyAnalysis.txt"


    if ($OnPremCAS) {
        Write-Header "CAS Role" -LogFile $LogFile
        $AutoDURI = @($OnPremCAS| Group-Object AutoDiscoverServiceInternalUri)
        Write-Log -Text($AutoDURI|Select-Object Name, Group |out-string) -OutputToConsole -LogFile $LogFile

        if ($AutoDURI.Count -eq $OnPremCAS.Count) {
            Write-Warning "Each CAS has a unqiue AutoDiscoverInternalUri"
        }
    }

    If ($OnPremAutodVdir) {
        Write-Header "AutoDiscover Virtual Directory - External Authentication Methods" -LogFile $LogFile
        Write-Log -Text($OnPremAutodVdir| Group-Object ExternalAuthenticationMethods -NoElement| Format-Table -AutoSize |out-string) -OutputToConsole -LogFile $LogFile
        Write-Header "AutoDiscover Virtual Directory - External URL" -LogFile $LogFile
        Write-Log -Text($OnPremAutodVdir| Group-Object ExternalURL -NoElement | Format-Table -AutoSize |out-string) -OutputToConsole -LogFile $LogFile
    }

    If ($OnPremWSVdir) {
        #External Auth
        Write-Header "Web Services Virtual Directory - External Authentication Methods" -LogFile $LogFile
        Write-Log -Text($OnPremWSVdir| Group-Object ExternalAuthenticationMethods -NoElement| Format-Table -AutoSize |out-string) -OutputToConsole -LogFile $LogFile
        Write-Header "Web Services Virtual Directory - External URL" -LogFile $LogFile
        Write-Log -Text($OnPremWSVdir| Group-Object ExternalURL -NoElement | Format-Table -AutoSize |out-string) -OutputToConsole -LogFile $LogFile
    }

    if ($CloudFedInfo) {
        Write-Header "Cloud Federation Information" -LogFile $LogFile
        Write-Log -Text($CloudFedInfo | Format-List |out-string) -OutputToConsole -LogFile $LogFile
    }
    if ($CloudFedOrgID) {
        Write-Header "Cloud Federation Org ID" -LogFile $LogFile
        Write-Log -Text($CloudFedOrgID| Format-List DefaultDomain, Domains, Enabled, DelegationTrustLink |out-string) -OutputToConsole -LogFile $LogFile
    }
    if ($OnPremFedOrgID) {
        Write-Header "On Premise Federation Org ID" -LogFile $LogFile
        Write-Log -Text($OnPremFedOrgID| Format-List DefaultDomain, Domains, Enabled, DelegationTrustLink |out-string) -OutputToConsole -LogFile $LogFile
    }
    if ($CloudFedTrust) {
        Write-Header "Cloud Federation Trust" -LogFile $LogFile
        Write-Log -Text($CloudFedTrust| Format-List ApplicationURI, OrgCertificate, TokenIssuerCertificate, TokenIssuerPrevCertificate |out-string) -OutputToConsole -LogFile $LogFile
    }
    if ($OnPremFedTrust) {
        Write-Header "On Premise Federation Trust" -LogFile $LogFile
        Write-Log -Text($OnPremFedTrust| Format-List ApplicationURI, OrgCertificate, TokenIssuerCertificate, TokenIssuerPrevCertificate |out-string) -OutputToConsole -LogFile $LogFile
    }
    If ($OnPremTestFedTrust) {
        Write-Header "Test Federation Trust" -LogFile $LogFile
        Write-Log -Text($OnPremTestFedTrust| Format-List |out-string) -OutputToConsole -LogFile $LogFile
    }

    if ($CloudOrgRel) {
        Write-Header "Cloud Organization Relationship" -LogFile $LogFile
        Write-Log -Text($CloudOrgRel| Format-List Name, DomainNames, FreeBusy*, Target*, Enabled |out-string) -OutputToConsole -LogFile $LogFile
    }

    if ($OnPremOrgRel) {
        Write-Header "On Premise Organization Relationship" -LogFile $LogFile
        Write-Log -Text( $OnPremOrgRel| Format-List Name, DomainNames, FreeBusy*, Target*, Enabled |out-string) -OutputToConsole -LogFile $LogFile
    }

    if ($OnPremTestFedTrustCert) {
        Write-Header "Test Federation Certificate" -LogFile $LogFile
        #$OnPremTestFedTrustCert| Format-Table -a Site, SErver, State, Thumbprint
        $FTCerr = $OnPremTestFedTrustCert|Where-Object {$_.State -ne "Installed"}
        if ($FTCerr) {
            Write-Header "Test Federation Certificate - Issues" -LogFile $LogFile
            Write-Log -Text($FTCerr| Format-List |out-string) -OutputToConsole -LogFile $LogFile
        }
        else {
            Write-Host "No Issues"
        }

    }
    else {Write-Header "No Test Federation Certificate Information" -LogFile $LogFile}

    if ($OnPremAvailAddrSpc) {
        #Availability Space
        Write-Header "On Premise Availability Address Space" -LogFile $LogFile
        Write-Log -Text($OnPremAvailAddrSpc| Sort-Object ForestName| Format-Table -a ForestName, AccessMethod, UserName, TargetAutodiscoverEPR, UseServiceAccount, Proxyurl |out-string) -OutputToConsole -LogFile $LogFile
    }
    if ($CloudAvailAddrSpc) {
        Write-Header "Cloud Availability Address Space" -LogFile $LogFile
        Write-Log -Text($CloudAvailAddrSpc| Sort-Object  ForestName| Format-Table -a ForestName, AccessMethod, UserName, TargetAutodiscoverEPR, UseServiceAccount, Proxyurl |out-string) -OutputToConsole -LogFile $LogFile
    }

    #Mailboxes
    If ($cloudMBX -and $OnPremRemoteMBX) {
        If ($cloudMbx.EmailAddresses -match $OnPremRemoteMBX.RemoteRoutingAddress) {
            Write-Header "Cloud Mailbox Remote Routing Address Match: True" -LogFile $LogFile
        }
        else {Write-Header "Cloud Mailbox Remote Routing Address Match: False" -LogFile $LogFile}
    }
    else { Write-Warning "Cloud Mailbox Information not present"}
    If ($cloudMBX -and $OnPremRemoteMBX) {
        If ($CloudOPMBX.ExternalEmailAddress -match $OnPremMBX.emailaddress) {
            Write-Header "On Premises Mailbox External Email Address Match: True" -LogFile $LogFile
        }
        else {Write-Header "On Premises Mailbox External Email Address Match: False" -LogFile $LogFile}
    }
    else { Write-Warning "On Premises Mailbox Information not present"}
}
Function _AnalyzeMigrationData () {
    Param (
        [string]$FilePath,
        [switch]$AnalyzeMoveReport = $false,
        [switch]$OutputAcceptedDomains = $false,
        [Switch]$Live
    )

    if ($Live) {
        [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic') | Out-Null
        $UPN = [Microsoft.VisualBasic.Interaction]::InputBox("Enter the user's UserPrincipalName in the format user@domain.com", "UPN")
        $CloudGetMigrationConfig = Get-MigrationConfig
        $CloudGetMigrationEndpoint = Get-MigrationEndpoint 
        $CloudGetMigrationBatch = Get-MigrationBatch
        #$CloudGetMoveRequest = Get-MoveRequest
        $CloudGetMigrationStatistics = Get-MigrationStatistics
        #$CloudMigrationBatchReport = Get-MigrationBatch -IncludeReport -Diagnostic
        $CloudMigrationUsers = Get-MigrationUser
        $MoveReport = Get-MoveRequestStatistics $UPN -IncludeReport -Diagnostic -DiagnosticArgument verbose
        $testmigrationendpoint = Get-MigrationEndpoint -Type ExchangeRemoteMove| ForEach-Object {Test-MigrationServerAvailability -Endpoint $_.Identity}
        $CloudAcceptedDomain = Get-AcceptedDomain
    }
    else {
        #Load data from files
        Open-XMLFiles -FilePath $FilePath
    }
    $LogFile = Join-Path -Path $FilePath -ChildPath "MigrationAnalysis.txt"

    if ($CloudAcceptedDomain) {
        Write-Header "Accepted Domains: $($CloudAcceptedDomain.count)" -LogFile $LogFile
        if ($OutputAcceptedDomains ) {
            Write-Log -Text ($CloudAcceptedDomain| Format-Table -a DomainName, DomainType|out-string) -OutputToConsole -LogFile $LogFile
        }
    }

    if ($CloudGetMigrationConfig) {
        Write-Log -Text ($CloudGetMigrationConfig|Select-Object Max*|out-string) -OutputToConsole -LogFile $LogFile
    }

    $count = @($CloudGetMigrationEndpoint).count
    Write-header "Migration Endpoints" -LogFile $LogFile
    for ($i = 0; $i -lt $count; $i++) {
        Write-Header "$($CloudGetMigrationEndpoint[$i].Identity) - $($CloudGetMigrationEndpoint[$i].RemoteServer)" -LogFile $logFile
        Write-Log -Text ($CloudGetMigrationEndpoint[$i]| Format-Table -a EndPointType, UserName, Max*, Active*|out-string) -OutputToConsole -LogFile $LogFile
        
        if ($testmigrationendpoint) {
            $ep = $testmigrationendpoint[$i]
            [xml]$epxml = $ep.ConnectionSettings
            if ($EP.Result -eq "Success") {
                write-Header ("Test-MigrationServerAvailability - EndPoint Successful - $($epxml.ExchangeConnectionSettings.IncomingExchangeServer)")
                Write-Log -Text ($epxml.ExchangeConnectionSettings| Format-List IncomingRPCPRoxyServer, IncomingExchangeServer, IncomingDomain, IncomingUserName, IncomingAuthentication, HasAdminPrivilege, HasAutoDiscovery, HasMRSProxy|out-string) -OutputToConsole -LogFile $LogFile
            }
            else {
                Write-Header ("Test-MigrationServerAvailability - $($ep.Message)") -LogFile $LogFile
                Write-Log -Text ($ep.Errordetail|out-string) -OutputToConsole -LogFile $LogFile
            }
        }
    }

    if ($CloudGetMigrationStatistics) {
        Write-Header "Migration Statistics" -LogFile $LogFile
        Write-Log -Text ($CloudGetMigrationStatistics|Select-Object MigrationType, *count|out-string) -OutputToConsole -LogFile $LogFile
    }

    if ($CloudGetMigrationBatch) {
        Write-header "Migration Batches" -LogFile $LogFile
        Write-Log -Text ($CloudGetMigrationBatch|Select-Object Identity, stat*, SourceEndpoint, MigrationType, BatchDirection, TotalCount, ActiveCount, StoppedCount, SyncedCount| Sort-Object -Object  BatchDirection, Identity | Format-Table -a|out-string) -OutputToConsole -LogFile $LogFile
    }

    if ($CloudMigrationUsers) {
        Write-Header "Failed Migration Users" -LogFile $Logfile
        Write-Log -Text ($CloudMigrationUsers |Where-Object {$_.State -eq "Failed"}| Format-List MailboxGuid, RecipientType, BatchID, ErrorSummary|out-string) -OutputToConsole -LogFile $LogFile
    }

    #Analyze Move Report
    if ($MoveReport -and $AnalyzeMoveReport -eq $true) {
        $stats = $MoveReport
        Write-Header "Move Request Details" -LogFile $LogFile
        Write-Log -Text ($Stats | Select-Object ExchangeGuid, Status, statusDetail, WorkLoadType, SourceVersion, SourceServer, RemoteHostName, RemoteCredentialUserName, StartTimeStamp, LastUpdateTimestamp, Failure*, Message|out-string) -OutputToConsole -LogFile $LogFile

        Switch ($stats.WorkloadType.value) {
            "LoadBalancing" {
                Write-Warning "This is an internal Load Balancing MoveRequest"
            }
            default {
                #Export Failures
                Write-Header "Failure Types" -LogFile $LogFile
                Write-Log -Text ($stats.Report.Failures.failureType| Group-Object -NoElement | Sort-Object  Count -Descending| Format-Table -AutoSize |out-string) -OutputToConsole -LogFile $LogFile

                Write-Header "Exported Failures and Entries to files for review" -LogFile $LogFile
                $stats.Report.Failures|Select-Object TimeStamp, Message|export-csv (Join-path -Path $filePath -ChildPath "$($stats.ExchangeGuid.Guid)__failures.csv") -NoTypeInformation
                #Export Entries
                $stats.Report.Entries|Select-Object LocalizedString|Export-Csv (Join-path -Path $filePath -ChildPath "$($stats.ExchangeGuid.Guid)_entries.txt")  -NoTypeInformation
                #Export BadItems
                #$stats.Report.BadItems | export-csv (Join-path -Path $filePath -ChildPath "$($stats.ExchangeGuid.Guid)_baditems.csv") -NoTypeInformation

                #Source Server Information
                $SourceCon = $stats.Report.Connectivity| Where-Object {$_.ServerKind -match "Source"}
                Write-Header "Source Server Version" -LogFile $LogFile
                Write-Log -Text (($SourceCon|Select-Object ServerVersionstr|Get-Unique -AsString).ServerVersionstr|out-string) -OutputToConsole -LogFile $LogFile
                Write-Header "MRS Proxy Server Version" -LogFile $LogFile
                Write-Log -Text (($SourceCon|Select-Object ProxyVersionstr|Get-Unique -AsString).ProxyVersionstr|out-string) -OutputToConsole -LogFile $LogFile
                #Proxy Servers Connections - If multiple servers this is bad
                $ProxyConn = $stats.Report.Connectivity| Where-Object {$_.ServerKind -match "Source"}| Sort-Object -Object  TimeStamp
                $sourceMRS = $ProxyConn| Group-Object ProxyName -NoElement

                if (($ProxyConn|Select-Object -First 1).ProxyName -ne ($ProxyConn|Select-Object -Last 1).ProxyName) {
                    #Write-Warning
                    Write-Warning ("Original MRS Proxy Server does not match the last attempted MRS Proxy Server")
                    Write-warning ("Original: {0}" -f (($ProxyConn|Select-Object -First 1).ProxyName))
                    Write-warning ("Lastest: {0}" -f (($ProxyConn|Select-Object -Last 1).ProxyName))
                }


                if (($sourceMRS).Values.Count -gt 1) {
                    Write-Warning "Detected Connections to multiple MRS Proxy Servers"
                    $sourceMRS| Format-Table -a
                }
            }  
        }
        #Check Bad Items
        if ($stats.Report.BadItems) {
            Write-Headers "BadItems" -LogFile $LogFile
        }

        #Check Large Items
        if ($stats.Report.MailboxVerification) {
            Write-Headers "Mailbox Verification Errors" -LogFile $LogFile
        }
 
        #Mailbox Size
        Write-Header "Mailbox Size" -LogFile $LogFile
        Write-Log -Text ($stats.report| Format-List *mailboxSize|out-string) -OutputToConsole -LogFile $LogFile

        #Throttles
        Write-Header "Throttles" -LogFile $LogFile
        Write-Log -Text ($stats.report| Format-List *throttles|out-string) -OutputToConsole -LogFile $LogFile
    }
}
Function _AnalyzeFocusedInbox() {
    Param (
        [string]$FilePath
    )
    if (!$FilePath) {
        $FilePath = _readFolderBrowserDialog
    }
    Open-XMLFiles -FilePath $FilePath
    $LogFile = Join-Path -Path $FilePath -ChildPath "FocusedInboxAnalysis.txt"

    IF ($OrgConfig.OAuth2ClientProfileEnabled -eq $true) {
        Write-Log -Text ("Modern Authentication is enabled for the Tenant") -OutputToConsole -LogFile $LogFile

        if ($CLMBx.IsEnabled -eq $true) { 
            Write-Log -Text ("Clutter is enabled for the user - Focused Inbox is disabled by default for the user" ) -OutputToConsole -LogFile $LogFile
        }
        ElseIf ($FIMbx.FocusedInboxOnLastUpdateTime -gt $OrgConfig.FocusedInboxOnLastUpdateTime) {
            $FIEnabled = $FIMbx.FocusedInboxOn
            $FIControl = "Mailbox"
        }
        elseIf ($FIMbx.FocusedInboxOnLastUpdateTime -lt $OrgConfig.FocusedInboxOnLastUpdateTime) {
            $FIEnabled = $OrgConfig.FocusedInboxOn
            $FIControl = "Tenant"
        }
        Write-Log -Text ("Focused Inbox On: $FIEnabled") -OutputToConsole -LogFile $LogFile
        Write-Log -Text ("Controlled By: $FIControl") -OutputToConsole -LogFile $LogFile
        Write-Log -Text ("Tenant TimeStamp: $($OrgConfig.FocusedInboxOnLastUpdateTime)") -OutputToConsole -LogFile $LogFile
        Write-Log -Text ("Mailbox TimeStamp: $($FIMbx.FocusedInboxOnLastUpdateTime)") -OutputToConsole -LogFile $LogFile
    }
    else {
        Write-Log -Text ( "Modern Authentication is NOT enabled for the Tenant - Focused Inbox will not be available") -OutputToConsole -LogFile $LogFile
    }
}
Function Test-CustomerConnector () {
    <#
 .Synopsis
 Creates a temporary Outbound Connector and runs the Validate-OutboundConnector function
 
.Description
The Test-CustomerConnector cmdlet creates a new Outbound Connector and runs the Validate-OutboundConnector command.
* SMTP connectivity to each smart host that's defined on the connector.
* Send test email messages to one or more recipients in the domain that's configured on the connector.
* You need to be assigned permissions before you can run this cmdlet. Although this topic lists all parameters for the cmdlet, you may not have access to some parameters if they're not included in the permissions assigned to you. To find the permissions required to run any cmdlet or parameter in your organization, see Find the permissions required to run any Exchange cmdlet.
 
 .Parameter RecipientDomain
The RecipientDomain parameter specifies the domain that the Outbound connector routes mail to.
 
 .Parameter SmartHosts
The SmartHosts parameter specifies the smart hosts the Outbound connector uses to route mail. This parameter is required if you set the UseMxRecord parameter to $false and must be specified on the same command line. The SmartHosts parameter takes one or more FQDNs, such as server.contoso.com, or one or more IP addresses, or a combination of both FQDNs and IP addresses. Separate each value by using a comma. If you enter an IP address, you may enter the IP address as a literal, for example: 10.10.1.1, or using Classless InterDomain Routing (CIDR), for example, 192.168.0.1/25. The smart host identity can be the FQDN of a smart host server, a mail exchange (MX) record, or an address (A) record.
 
.Parameter TLSDomain
The TlsDomain parameter specifies the domain name that the Outbound connector uses to verify the FQDN of the target certificate when establishing a TLS secured connection. This parameter is only used if the TlsSettings parameter is set to DomainValidation. Valid input for the TlsDomain parameter is an SMTP domain. You can use a wildcard character to specify all subdomains of a specified domain, as shown in the following example: *.contoso.com. However, you can't embed a wildcard character, as shown in the following example: domain.*.contoso.com
 
 .Parameter TLSSettings
The TlsSettings parameter specifies the TLS authentication level that's used for outbound TLS connections established by this Outbound connector. Valid values are:
* EncryptionOnly TLS is used only to encrypt the communication channel. No certificate authentication is performed.
* CertificateValidation TLS is used to encrypt the channel and certificate chain validation and revocation lists checks are performed.
* DomainValidation In addition to channel encryption and certificate validation, the Outbound connector also verifies that the FQDN of the target certificate matches the domain specified in the TlsDomain parameter.
* $null (blank) This is the default value.
 
 .Parameter Delay
  Number of seconds to pause between tests
 
 .Parameter Count
  Number of times to run the test
 
 .Example
    Test-CustomerConnector -RecipientDomain contoso.com -SmartHosts mail.contoso.com -TLSDomain *.contoso.com -TLSSettings DomainValidation
 
 .Example
    Test-CustomerConnector -RecipientDomain contoso.com -SmartHosts mail.contoso.com -TLSSettings EncryptionOnly
 
 .Example
    Test-CustomerConnector -RecipientDomain contoso.com -SmartHosts mail.contoso.com -TLSDomain *.contoso.com -TLSSettings DomainValidation
 
#>


    Param
    (
        [Parameter(Mandatory = $true)][string]$RecipientDomain,
        [Parameter(Mandatory = $true)][string[]]$SmartHosts,
        [Parameter(Mandatory = $false)][string]$Recipient,
        [Parameter(Mandatory = $false)][string]$TLSDomain,
        [Parameter(Mandatory = $false)][ValidateSet("EncryptionOnly", "CertificateValidation", "DomainValidation")][string]$TLSSettings = $null,
        [Parameter(Mandatory = $false)][int]$Delay = 60,
        [Parameter(Mandatory = $false)][int]$Count = 20
    )

    $guid = [guid]::NewGuid()
    $OCParam = @{Name = "TempConnector"; RecipientDomains = $RecipientDomain; SmartHosts = $SmartHosts; UseMXRecord = $false}
    if ($TLSDomain -or $TLSSettings -eq "DomainValidation") {
        $OCParam.add("TLSDomain", $TLSDomain);
        $OCParam.add("TLSSettings", "DomainValidation")
    }
    elseif ($TLSSettings -eq "CertificateValidation" -or $TLSSettings -eq "EncryptionOnly") {
        $OCParam.add("TLSSettings", "$TLSSettings")
    }
    #Remove lingering object
    Remove-OutboundConnector $OCParam.name -Confirm:$false -WarningAction SilentlyContinue -errorAction SilentlyContinue    
    $oc = New-OutboundConnector @OCParam -WarningAction SilentlyContinue

    if ($Recipient -eq "") {
        $Recipient = "user-$($guid)@$($RecipientDomain)"
    }

    if ($oc) {
        Write-host "Created New Outbound Connector"
        $OC| Format-List RecipientDomains, SmartHosts, TlsDomain, TLSSettings
        #Loop for test if the outbound Connector was created
        for ($i = 1; $i -le $Count; $i++) {
            Write-host "$(get-date) - Processing $($i) of $($Count)"
            Write-host $delim
            $results = Validate-OutboundConnector $OCParam.name -Recipients "user-$($guid)@$($RecipientDomain)"

            _parseTasks -TaskResult $results
            if ($results.IsTaskSuccessful -eq $true) {$i = $count; $delay = 0}
            Start-Sleep -Seconds $Delay
        }
        Remove-OutboundConnector $OCParam.name -Confirm:$false -WarningAction SilentlyContinue -errorAction SilentlyContinue
    }
}

function _parseTasks() {
    Param (
        [Parameter(Mandatory = $true)]$TaskResult
    )
    Foreach ($st in $TaskResult) {
        Write-header $st.TaskName
        Write-host $st.taskDetail
        if ($st.SubTaskResults) {
            #Recurse
            _parseTasks -TaskResult $st.SubTaskResults
        }
    }
}
function Get-SafeSendersHash {
    Param ([string] $email)
   
    ##Convert to SHA256 Hash
    if ($null -ne $email -and $email.length -gt 0) {

        $hash = [System.Security.Cryptography.sha256]::create().computehash("$email".ToCharArray())
        Write-Host "sha256: $hash"
    }

    ## Write-Host "tried to calc hash: $hash"
    if ($hash -eq $null -or $hash.count -eq 0) {
        write-host "Error creating hash"
        return 1
    }

    ##GetBytes 4 - 7 (Second 4 bytes)
    ([BitConverter]::ToString(@($hash[7], $hash[6], $hash[5], $hash[4])) -replace '-', '')
}
function _NewRPS () {
    Param (
        [string]$ConnectionURI,
        [string]$Title, 
        [switch]$SkipImport = $False, 
        [string]$Prefix, 
        [string]$Target
    )
    write-Host "Connecting to $($title)..."
    $Credential = Get-CachedCredential -Target $Target
    $sessionOpts = New-PSSessionOption -IdleTimeout 3600000 -OpenTimeout 0
    $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri $ConnectionURI -Credential $Credential -Authentication Basic -AllowRedirection -SessionOption $sessionOpts -ErrorAction silentlycontinue -WarningAction SilentlyContinue
    if (!$session) {
        $Credential = Get-CachedCredential -Target $Target -Delete
        $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri $ConnectionURI -Credential $Credential -Authentication Basic -AllowRedirection -SessionOption $sessionOpts -ErrorAction stop -WarningAction SilentlyContinue
    }
    if ($Skipimport -eq $false) {
        if ($Prefix) {
            Import-Module (Import-PSSession $Session -DisableNameChecking -WarningAction SilentlyContinue -Prefix $prefix -ErrorAction SilentlyContinue) -Global -DisableNameChecking -Prefix $Prefix
            Write-Warning "All commands prefixed with $($Prefix)"
        }
        else {Import-Module (Import-PSSession $Session -DisableNameChecking -WarningAction SilentlyContinue -ErrorAction SilentlyContinue) -Global -DisableNameChecking}
    }
}
Function Connect-ExchangeOnline () {
    <#
 .Synopsis
 Create a new remote PowerShell session to Exchange Online
 
 .Description
  Create remote PowerShell session to Exchange Online
  Connect to AzureAD and Microsoft Online if installed
  Store/Retrieve credentials from Credential Manager for easy use
  
 .Parameter Target
  Name for the Key in Credential Manager
 .Parameter Prefix
 Prefix to add to the imported commands
 .Parameter TargetMailbox
 Add the target mailbox to the ConnectionURI.
 This is usefull in a Multi-Geo configuration to force the connection to the Target Mailbox's forest.
 .Example
 Connect-ExchangeOnline
 Store/Retrieve the default credentials from Credential Manager
 
 .Example
 Connect-ExchangeOnline -Target CloudAdmin
 Store/Retrieve the specific credentials from Credential Manager
 
 .Example
 Connect-ExchangeOnline -TargetMailbox EMEAUser@contoso.com
 Establish Remote PowerShell session to using the EMEAUser@contoso.com mailbox as the location
 
    #>

    Param(
        [string]$Target = "CloudShell",
        [string]$Prefix,
        [switch]$SkipAzureAD=$false,
        [String]$TargetMailbox
    )
if ($TargetMailbox) {
    $uri = "$($ConnectionURI.ExchangeOnline)?email=$($TargetMailbox)"
}
else {
    $uri = $ConnectionURI.ExchangeOnline
}
Write-Verbose $URI
if ($prefix) {
    _NewRPS -ConnectionURI ($uri) -Title "Exchange Online" -Import -Target $Target -Prefix $Prefix
}
else {
    _NewRPS -ConnectionURI ($uri) -Title "Exchange Online" -Import -Target $Target
}
 if ($SkipAzureAD -eq $false){_ConnectAAD -Target $Target }
}
Function Connect-ExchangeOnPrem() {
    <#
 .Synopsis
 Create a new remote PowerShell session to an Exchange OnPrem
.Description
  Create remote PowerShell session to an Exchange OnPrem system
  Store/Retrieve PowerShell URL from Credential Manager for easy use
  Store/Retrieve credentials from Credential Manager for easy use
 
 .Parameter Target
  Name for the Key in Credential Manager
 
 .Parameter Prefix
 Prefix to add to the imported commands
   
 .Example
    Connect-ExchangeOnPrem
    Store/Retrieve the default credentials from Credential Manager
 
    .Example
    Connect-ExchangeOnPrem -Prefix Local
    Store/Retrieve the default credentials from Credential Manager
    Import the commands and prefix each command with the value provided
 
     .Example
    Connect-ExchangeOnPrem -Target OnPrem
    Store/Retrieve the specific credentials from Credential Manager
 
    .Example
    Connect-ExchangeOnPrem -URL https://mail.contoso.com/PowerShell
    Connect to the specific URL
    #>

    Param (
        [string]$Target = "OnPremShell",
        [string]$Prefix,
        [string]$URL
    )
    
    #Look for OnPrem URL in Documents and load into ConnectionURI
    if (!$URL) {$URL = _OnPremFQDN}
    if ($Prefix) {
        _NewRPS -ConnectionURI $URL -Title "Exchange OnPrem" -Import -Prefix $Prefix -Target $Target
    }
    else {
        _NewRPS -ConnectionURI $URL -Title "Exchange OnPrem" -Import -Target $Target
    }
}
Function Connect-ComplianceCenter() {
    <#
 .Synopsis
 Create a new remote PowerShell session to Security and Compliance Center
 
 .Description
  Create remote PowerShell session to Security and Compliance Center
  Connect to AzureAD and Microsoft Online if installed
  Store/Retrieve credentials from Credential Manager for easy use
  
 .Parameter Target
  Name for the Key in Credential Manager
     
  .Example
    Connect-ComplianceCenter
    Store/Retrieve the default credentials from Credential Manager
 
  .Example
    Connect-ComplianceCenter -Target CloudAdmin
    Store/Retrieve the specific credentials from Credential Manager
 
    #>

    Param(
        [string]$Target = "CloudShell"
    )
    
    _NewRPS -ConnectionURI ($ConnectionURI.ComplianceCenter)  -Title "Compliance Center" -Import -Target $Target
    _ConnectAAD -Target $Target
}
Function _ConnectAAD () {
    Param(
        [string]$Target
    )
    $Cred = Get-CachedCredential -Target $Target
    $AADModules = get-module -ListAvailable AzureAD*
    if ($AADModules) {
        Import-Module $AADModules.Name
        Write-Host "Connecting to Azure AD..."
        Connect-AzureAD -Credential $Cred | out-null
    }
    $Cred = Get-CachedCredential -Target $Target
    if (Get-Module -ListAvailable MSOnline) {
        Import-Module MSOnline
        Write-Host "Connecting to Microsoft Online..."
        Connect-MsolService -Credential $Cred | Out-Null
    }
}
Function _OnPremFQDN() {
    try {
        $Cred = Get-StoredCredential -Target "OnPremURL" -ErrorAction SilentlyContinue    
    }
    catch {
        Write-Host "Could not find stored credential"
    }
    
    if ($Cred) {
        Return $cred.UserName
    }
    else {
        [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic') | Out-Null
        $RemoteAddress = [Microsoft.VisualBasic.Interaction]::InputBox("OnPremises PowerShell FQDN (Https://owa.contoso.com/PowerShell", "OnPremises PowerShell URL")
        New-StoredCredential -Target "OnPremURL" -UserName $RemoteAddress -Password "URL" -Type GENERIC -Persist LOCALMACHINE|out-null
        Return $RemoteAddress
    }
}

Function Test-AuthenticationEndPoint() {
    <#
 .Synopsis
 Test Authentication Endpoints for known issues
 
 .Description
Queries Office 365 for the RealmDiscovery XML and performs various checks on the configured URLS
Helps determine what system a domain uses for Authentication (ADFS, Okta, SecureAuth, Azure AD, etc...)
 
 .Parameter Domain
  Domain to check for the RealmDiscovery XML
 .Parameter LaunchURLs
  Launch the URLs in a browser window
  
 .Example
    Test-AuthenticationEndPoint -Domain Contoso.com
 .Example
    Test-AuthenticationEndPoint -Domain Contoso.com -LaunchURLs
    Performs the tests and launchs the URLs in the default browser
 
    #>
[CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True)][string]$Domain,
        [Parameter(Mandatory = $False)][Switch]$LaunchURLs
    )
       $results = @()
    #Check Realm Discovery XML
    Write-verbose "Checking Realm Discovery XML..."
    [xml]$RealmDiscovery = Invoke-WebRequest "https://login.microsoftonline.com/GetUserRealm.srf?Login=user@$domain&xml=1" -ErrorVariable RealmResponse

    $RealmTest = New-Object PSObject
    Add-Member -input $RealmTest noteproperty 'Domain' $Domain
    Add-Member -input $RealmTest noteproperty 'Type' "Unknown"
    Add-Member -input $RealmTest noteproperty 'Endpoint' ""

    #Skip URL checks - This will be set to false for known systems
    $SkipURLCheck = $True

    #Query the Realm Info to determine the Domain Type and IDP type
    if ($RealmDiscovery.realminfo.NameSpaceType -eq "Federated") {
        $STSDomain = $RealmDiscovery.realminfo.AuthUrl.Substring($RealmDiscovery.realminfo.AuthUrl.IndexOf('/') + 2)
        $RealmTest.Endpoint = $STSDomain.Substring(0, $STSDomain.IndexOf('/'))

        if (!$RealmDiscovery.realminfo.AuthUrl.Contains("$($RealmTest.Endpoint)/adfs/ls/")) {
            if ($RealmDiscovery.RealmInfo.AuthURL -match ".Okta.com") {
                Write-Verbose "Customer is using Okta"
                $RealmTest.Type = "Okta"
                $SkipURLCheck = $False
            }
            elseif ($RealmDiscovery.RealmInfo.AuthURL -match "SecureAuth") {
                Write-Verbose "Customer is using SecureAuth"
                $RealmTest.Type = "SecureAuth"
            }
            elseif ($RealmDiscovery.RealmInfo.MEXURL -match "mex.ping") {
                Write-Verbose "Customer is using Ping Federate"
                $RealmTest.Type = "Ping Federate"
                $SkipURLCheck = $False
            }
            else {
                Write-Verbose "Customer is not using ADFS"
            }
        }
        else {
            $RealmTest.Type = "ADFS"
            $SkipURLCheck = $false
        }

        #Check TCP Connectivity
        $result=  _CheckTCPConnectivity $RealmTest.Endpoint
        $results+=$result
        if ($result.Status -eq "Pass" -and $SkipURLCheck -eq $false) {
            #Start URL tests if TCP check passes
            if ($result) {
                #Check AuthURL
                $results += _CheckURL -URL $RealmDiscovery.RealmInfo.AuthURL -URLType "AuthURL"     

                #Check STSAuthURL
                $results += _CheckURL $RealmDiscovery.RealmInfo.STSAuthURL -URLType "STSAuthURL"

                #Check MEXURL
                $results += _CheckURL $RealmDiscovery.RealmInfo.MEXURL -URLType "MEXURL"

                if ($LaunchURLS -eq $true) {
                    Start-Process "https://login.microsoftonline.com/GetUserRealm.srf?Login=user@$domain&xml=1"
                    Start-Process $RealmDiscovery.RealmInfo.AuthURL
                    Start-Process $RealmDiscovery.RealmInfo.STSAuthURL
                    Start-Process $RealmDiscovery.RealmInfo.MEXURL
                }
            }
        }
    }
 
    elseif ($RealmDiscovery.RealmInfo.NameSpaceType -eq "Managed") {
        Write-Verbose "Customer is using Managed Authentication and/or PasswordSync"
        $RealmTest.Type = "Managed"
        $RealmTest.Endpoint = "Login.microsoftonline.com"
    }
    else {
        Write-warning "Domain is NOT registered in Office 365"
        $RealmTest = $null
    }
    #Output test results
    if ($RealmTest) {
        Write-Header "Endpoint Infomation"
        $RealmTest | Format-Table -AutoSize
        if ($RealmTest.Type -ne "ADFS" -and $RealmTest.Type -ne "Managed") {
            Write-Warning "This could be a 3rd-party issue"
        }
    }
    if ($results) {
        Write-Header "Endpoint Test Results"
        $Results| Format-Table Type, Status, Endpoint -AutoSize
        $failures = $Results | Where-Object {$_.Status -eq "Fail"}
        if ($failures) {
            Write-Header "Failures"
            $Failures | Format-List Type,Message
        }
    }
    Write-Verbose "End of tests"
}

Function _CheckTCPConnectivity () {
    #internal function
    Param (
        [Parameter(Mandatory = $True)][string] $server
    )
    $Test = New-Object PSObject
    Add-Member -input $Test noteproperty 'Type' "TCP Connectivity"
    Add-Member -input $Test noteproperty 'Status' ""
    Add-Member -input $Test noteproperty 'Endpoint' $Server 
    Add-Member -input $Test noteproperty 'Message' ""   
    Write-Verbose "Attempting to connect to $server on port 443"
    $socket = new-object Net.Sockets.TcpClient
    try {
        $socket.Connect($server, 443)
    }
    catch {
        $tcperror = $_.Exception.Message
        $Test.Message = $tcperror = $tcperror.Substring($tcperror.IndexOf(':') + 2).Trim()
    }
    if ($socket.Connected) {
        $socket.Dispose()
        Write-Verbose "Connection Successful"
        $Test.Status = "Pass"

    }
    else {
        Write-Verbose "Connection failed"
        $Test.Status = "Fail"
    }
    #return the test results
    $test
}
Function _CheckURL() {
    #internal function
    Param (
        [Parameter(Mandatory = $True)][string]$URL,
        [Parameter(Mandatory = $True)][ValidateSet("AuthURL", "STSAuthURL", "MEXURL")][string]$URLType
    )
    $Test = New-Object PSObject
    Add-Member -input $Test noteproperty 'Type' $URLType
    Add-Member -input $Test noteproperty 'Status' ""
    Add-Member -input $Test noteproperty 'EndPoint' $($URL)    
    Add-Member -input $Test noteproperty 'Message' ""   

    Write-Verbose "Checking $URLType"
    Try {
        $r = Invoke-WebRequest $URL -ErrorVariable Response
    }
    Catch {}

    switch ($URLType) { 
        "AuthURL" {
            if ($Response -ne $null) {
                $Test.Status = "Fail"
                $Test.Message = "$($Response[0].Message)"
            }
            else {
                $Test.Status = "Pass"
            }
        } 
        "STSAuthURL" {
            if ($Response[0] -ne $null) {
                if ($Response[0].Message.Contains("(400) Bad Request.")) {
                    $Test.Status = "Pass"

                }
                elseif ($Response[0].Message.Contains("The remote server returned an error: (500) Internal Server Error.") -and $URL -match "/sts.wst") {
                    $Test.Status = "Pass"
                }
                else {
                    $Test.Status = "Fail"
                    $Test.Message = "$($Response[0].Message)"
                }
            }
            elseif ($r.Content -match "The request was invalid or malformed" -and $URL -match "okta.com") {
                $Test.Status = "Pass"
            }
            else {
                    $Test.Status = "Fail"
                    $Test.Message = "Unexpected response received"
            }
        } 
        "MEXURL" {
            if ($Response -ne $null) {
                $Test.Status = "Fail"
                $Test.Message = "$($Response[0].Message)"
            }
            else {
                $Test.Status = "Pass"
            }
        }  
    }
    #Return the test results
    $Test
}

#Transport/MailFlow
Function _AnalyzeMailFlow () {
    Param (
        [string]$FilePath
    )
    Open-XMLFiles -FilePath $FilePath
    $LogFile = Join-Path -Path $FilePath -ChildPath "MailFlowAnalysis.txt"

    #Check Send Connectors
    foreach ($Conn in ($SendConnectors | Where-Object {$_.Addressspaces -match "mail.onmicrosoft.com"})) {
        #Check Hybrid Connectors
        Write-Header "Send Connector Name: $($Conn.Name)" -LogFile $LogFile
        If ($conn.CloudServicesMailEnabled -eq $false) { Write-Log "CloudServicesMailEnabled is set to False. This should be set to True" -OutputToConsole -LogFile $LogFile}
        Write-log ($Conn|Format-List Enabled, Fqdn, AddressSpaces,MaxMessageSize, ProtocolLoggingLevel, SourceTransportServers, RequireTLS, TLSDomain, TLSAuthLevel, TLSCertificateName, WhenCreatedUTC, WhenChangedUTC|out-string) -OutputToConsole -LogFile $LogFile
    }
    
    foreach ($Conn in ($SendConnectors | Where-Object {$_.SmartHosts -match "mail.protection.outlook.com"})) {
        #Check EOP Connectors
        Write-Header "Send Connector Name: $($Conn.Name)" -LogFile $LogFile
        if ($conn.SmartHostAuthMechanism.Value -ne "None") { Write-Log ("SmartHostAuthMechanism is set incorrectly: $($conn.SmartHostAuthMechanism)") -OutputToConsole -LogFile $LogFile }
        Write-Log ($conn|Format-List Enabled, SmartHostsString, Fqdn, AddressSpaces,MaxMessageSize, ProtocolLoggingLevel, SourceTransportServers, RequireTLS, TLSDomain, TLSAuthLevel, TLSCertificateName, WhenCreatedUTC, WhenChangedUTC|out-string) -OutputToConsole -LogFile $LogFile
    }

    foreach ($Conn in ($SendConnectors | Where-Object {$_.SmartHosts -notmatch "mail.protection.outlook.com" -and $_.Addressspaces -notmatch "mail.onmicrosoft.com"})) {
        #Check Non-EOP/Hybrid Connectors
        Write-Header "Send Connector Name: $($Conn.Name)" -LogFile $LogFile
        if ($conn.SmartHostAuthMechanism.Value -ne "None") { Write-Log ("SmartHostAuthMechanism is set incorrectly: $($conn.SmartHostAuthMechanism)") -OutputToConsole -LogFile $LogFile }
        Write-Log ($conn|Format-List Enabled, SmartHostsString, Fqdn, AddressSpaces,MaxMessageSize, ProtocolLoggingLevel, SourceTransportServers, RequireTLS, TLSDomain, TLSAuthLevel, TLSCertificateName, WhenCreatedUTC, WhenChangedUTC|out-string) -OutputToConsole -LogFile $LogFile
    }

    #Check Receive Connectors for TLSDomainCapabilities
    $EOPRCs = $ReceiveConnectors| Where-Object {$_.TLSDomainCapabilities -ne $null}
    if ($EOPRCs) {
        ForEach ($Conn in $EOPRCs) {
            Write-Header "Receive Connector Name: $($Conn.Identity)" -LogFile $LogFile
            Write-log ($Conn|Format-List Enabled, Fqdn, Bindings, RemoteIPRanges, MaxMessageSize, ProtocolLoggingLevel, RequireTLS, TLSCertificateName, AuthMechanism|out-string) -OutputToConsole -LogFile $LogFile
        }
    }
    else {
        Write-Log "No Receive Connectors have TLSDomainCapabilities configured" -OutputToConsole -LogFile $LogFile
    }
    #Check for Receive Connectors with ExternalAuthoritative
    $EA = $ReceiveConnectors | Where-Object {$_.AuthMechanism -match "ExternalAuthoritative"}
    if ($EA) {
        Write-Header "The following receive connectors have ExternalAuthoritative enabled" -LogFile $LogFile
        Write-Log ($EA|Format-List Name, Server, RemoteIPRanges|out-string) -OutputToConsole -LogFile $LogFile
    }

    #Outbound Connectors
    foreach ($Conn in ($OutboundConnectors |Sort-Object Name)) {
        Write-Header "Outbound Connector Name: $($Conn.Name)" -LogFile $LogFile
        Write-Log ($Conn| Format-List Enabled, ConnectorType, RecipientDomains, AllAcceptedDomains, SmartHosts, UseMXRecord, CloudServicesMailEnabled, TLSDomain, TLSSettings, RequireTLS, IsTransportRuleScoped, WhenCreatedUTC, WhenChangedUTC|out-string) -OutputToConsole -LogFile $LogFile
    }

    if ($OutboundConnectors | Where-Object {$_.RouteAllMessagesViaOnPremises -eq $true}) {
        #Warn about Centralized Mail Transport
        Write-Log -Text "WARNING: Centralized Mail Transport (CMT) is currently enabled" -OutputToConsole -LogFile $LogFile
    }

    #Inbound Connectors
    #IP Based
    foreach ($Conn in ($InboundConnectors | Where-Object {$_.SenderIPAddresses -ne $null}|Sort-Object Name)) {
        Write-Header "Inbound Connector Name: $($Conn.Name)" -LogFile $LogFile
        Write-Log ($Conn|Format-List Enabled, ConnectorType, CloudServicesMailEnabled, TreatMessagesAsInternal, RequireTLS, SenderDomains, AssociatedAcceptedDomains, SenderIPAddresses, RestrictDomainsToIPAddresses, WhenCreatedUTC, WhenChangedUTC|out-string) -OutputToConsole -LogFile $LogFile
    }
    
    #Certificate Based
    foreach ($Conn in ($InboundConnectors | Where-Object {$_.TlsSenderCertificateName -ne $null}|Sort-Object Name)) {
        Write-Header "Inbound Connector Name: $($Conn.Name)" -LogFile $LogFile
        Write-Log ($Conn|Format-List Enabled, ConnectorType, CloudServicesMailEnabled, TreatMessagesAsInternal, RequireTLS, SenderDomains, AssociatedAcceptedDomains, TlsSenderCertificateName, RestrictDomainsCertificates, WhenCreatedUTC, WhenChangedUTC|out-string) -OutputToConsole -LogFile $LogFile
    }

    If ($ExchangeServers) {
        #Exchange Server Information
        Write-Header "Exchange Servers" -LogFile $LogFile
        Write-Log ($ExchangeServers |Sort-Object Name |Format-List Name, ServerRole, Site, AdminDisplayVersion|Out-String) -OutputToConsole -LogFile $LogFile
    }

    if ($TransportServers) {
        #Transport Servers Information
        Write-Header "Transport Servers" -LogFile $LogFile
        Write-Log ($TransportServers | Sort-Object Name| Format-List Name, ConnectivityLogPath, ReceiveProtocolLogPath, SendProtocolLogPath, InternalTransportCertificateThumbprint|out-string) -OutputToConsole -LogFile $LogFile
    }

    if ($OnPremAcceptedDomains) {
        Write-Header "Exchange OnPrem Accepted Domains" -LogFile $LogFile
        Write-log ($OnPremAcceptedDomains |Sort-Object Name| Format-Table -AutoSize DomainName, DomainType, IsDefaultFederatedDomain|Out-String) -OutputToConsole -LogFile $LogFile
    }
    if ($CloudAcceptedDomains) {
        Write-Header "Exchange Online Accepted Domains" -LogFile $LogFile
        Write-log ($CloudAcceptedDomains |Sort-Object Name| Format-Table -AutoSize DomainName, DomainType, Default, FederatedOrganizationLink, IsCoexistenceDomain|Out-String) -OutputToConsole -LogFile $LogFile     
    }


    # $diffAD = Compare-Object -ReferenceObject $OnPremAcceptedDomains -DifferenceObject $CloudAcceptedDomains -Property Name
    # if ($diffAD) {
    # Write-Header "Accepted Domains that only exist in Exchange OnPrem" -LogFile $LogFile
    # Write-Log (($diffAD| Where-Object {$_.SideIndicator -eq "<="}).Name|out-string) -OutputToConsole -LogFile $LogFile
    # Write-header "Accepted Domains that only exist in Exchange Online" -LogFile $LogFile
    # write-log (($diffAD| Where-Object {$_.SideIndicator -eq "=>"}).Name|out-string) -OutputToConsole -LogFile $LogFile
    # }
    If ($OnPremRemoteDomains) {
        Write-Header "Exchange OnPrem Remote Domains" -LogFile $LogFile
        Write-Log ($OnPremRemoteDomains|Sort-Object Name| Format-Table -AutoSize  DomainName, IsInternal, TrustedMailOutboundEnabled, TrustedMailInboundEnabled|Out-String) -OutputToConsole -LogFile $LogFile
    }
    if ($CloudRemoteDomains) {
        Write-Header "Exchange Online Remote Domains" -LogFile $LogFile
        Write-Log ($CloudRemoteDomains|Sort-Object Name| Format-Table -AutoSize  DomainName, IsInternal, TrustedMailOutboundEnabled, TrustedMailInboundEnabled|Out-String) -OutputToConsole -LogFile $LogFile
    }

    # Write-Header "Exchange Online Accepted Domains that are NOT configured for Hybrid Mail Flow" -LogFile $LogFile
    # Write-Log ((Compare-Object -referenceObject (($onpremremotedomains | Where-Object {$_.TrustedMailInboundEnabled -eq $true}).DomainName.Domain) -differenceObject ($CloudAcceptedDomains.DomainName) | Where-Object {$_ -notmatch "Mail.onmicrosoft.com" -and $_.SideIndicator -eq "=>"}).InputObject|out-string) -OutputToConsole -LogFile $LogFile

    if ($CloudTransportrules) {
        Write-Header "Exchange Online Transport Rules" -LogFile $LogFile
        Write-Log ($cloudtransportrules| Format-List Priority,Name,Description,State,StopRuleProcessing,Mode,SetAuditSeverity,RuleErrorAction,Guid,WhenChanged|Out-string) -OutputToConsole -LogFile $LogFile

    }
    Write-Header "Checking current queues" -LogFile $LogFile
    If ($queues) {
        #Current Queues
        Write-Log -Text($queues| Format-Table -AutoSize Identity, Status, DeliveryType, NextHopDomain, MessageCount|Out-String) -OutputToConsole -LogFile $LogFile

        #Queues with Errors
        $qerrors = $queues |Where-Object {$_.LastError -ne ""}
        if ($qerrors) {
            Write-header "Queues with Errors" -LogFile $LogFile
            Write-Log -Text($qerrors| Format-List Identity, Status, DeliveryType, NextHopDomain, MessageCount, LastError|Out-String) -OutputToConsole -LogFile $LogFile
        }
    }
    Else {
        Write-Log -Text "There are no queued messages" -OutputToConsole -LogFile $LogFile
    }

}

function _GenerateText([int]$Min, [int]$Max, [string[]]$Source, [switch]$ProperCase = $false) {
    $outLength = Get-Random -Minimum $min -Maximum $max
    [string]$txt = ""
    For ($i = 0; $i -lt $outlength; $i++) {
        $txt = (get-random -inputObject $Source) + " " + $txt
    }
    # Capitalise the first character of the subject line
    if ($ProperCase) {
        $txt = $txt.substring(0, 1).ToUpper() + $txt.substring(1)
    }
    return $txt
} 
Function Start-MailGenerator() {
    <#
.Synopsis
 Generates mail flow items using Exchange Web Services
 
 .Description
  Generates mail flow items using Exchange Web Services.
  The subject of the item is determined byt the Dictionary parameter and can be loaded from a text file or provided on the command line
  The contents of the item is determined byt the Text parameter and can be loaded from a text file or provided on the command line
  The following actions can be done by the script
    * Create new email items
    * Create meeting invites
    * Reply to existing unread emails and mark them as read
    * Accept/Decline received meeting invites
 
 
.Parameter SchemaType
Exchange Web Services Schema Type
 
.Parameter UseAutoDiscover
Use Autodiscover to determine the Exchange Web Services URL
 
.Parameter AutoDiscoverEmailAddress
Email Address to use for AutoDiscover
 
.Parameter EWSURL
Exchange Web Services URL - Defaults to Exchange Online - https://outlook.office365.com/ews/exchange.asmx
 
.Parameter EndPoint = "ExchangeOnline"
Endpoint name - ExchangeOnline or ExchangeOnPrem - Used for reporting purposes
 
.Parameter Credentials
Credentials to use when logging into Exchange Online
 
.Parameter NumberOfItems
Number of items to generate
 
.Parameter Senders
List of sender email addresses
 
.Parameter Recipients
List of recipient email addresses
 
.Parameter SubjectMinLength
Minimum number of words to use in the subject
 
.Parameter SubjectMaxLength
Maximum number of words to use in the subject
 
.Parameter BodyMinLines
Minimum number of lines to use in the body of the item
 
.Parameter BodyMaxLines
Maximum number of lines to use in the body of the item
 
.Parameter MaxRecipients
Maximum number of recipients to add to each item
 
.Parameter Dictionary
Content to use for the subject
 
.Parameter Text
Content to use for the body of the item
 
.Parameter UploadAttachments
Add attachments to the items
 
.Parameter AttachmentsDirectory
Directory where the attachments are located
 
.Parameter CreateMeetingInvites
Generate meeting invites
 
.Parameter MeetingMinDays
Mininum number of days to schedule ahead
 
.Parameter MeetingMaxDays
Maximum number of days to schedule ahead
 
.Parameter MeetingMinHour
Earliest starting hour for the meeting invites (Do not schedule before this time)
 
.Parameter MeetingMaxHour
Latest ending hour for the meeting invites (Do not schedule after this time)
 
.Parameter MeetingMinStart
Minute portion on when to schedule meeting
 
.Parameter MeetingLength
Different lengths of meetings in minutes
 
.Parameter MeetingPercentage
General percentage of items that should be meeting invites
 
.Example
PS C:\>[string[]]$text = get-content text.txt
PS C:\>[string[]]$Dict = get-content dict.csv
PS C:\>#Load EXO Senders and Credentials
PS C:\>$Senders = Get-content Senders.txt
PS C:\>$Creds = Get-CachedCredential -Target "MailGen"
PS C:\>#Load Recipients
PS C:\>$recipients = Get-Content recipients.txt
PS C:\>Start-MailGenerator -Senders $Senders -Recipients $Recipients -Dictionary $dict -Text $text -Credentials $Creds -NumberOfItems 100 -CreateMeetingInvites
 
 
.Example
PS C:\>Start-MailGenerator -Senders "user@contoso.com","user2@contoso.com" -Recipients "user10@contoso.com","user12@contoso.com","user@contoso.com","user2@contoso.com" -Dictionary "Test","Email","Subject" -Text "Line 1","Line 2", "Line 3" -Credentials $Creds -NumberOfItems 10
This examples will generate 10 items from the 2 different senders to a combination of the 4 recipients
#>

    [CmdletBinding(DefaultParameterSetName = "Default")]
    Param (
        [Parameter(Mandatory = $false)][String]$SchemaType = "Exchange2010_SP1",
        [Parameter(Mandatory = $true, ParameterSetName = "Autodiscover")][switch]$UseAutoDiscover = $false,
        [Parameter(Mandatory = $true, ParameterSetName = "Autodiscover")][string]$AutoDiscoverEmailAddress = $false,
        [Parameter(Mandatory = $false)][string]$EWSURL = "https://outlook.office365.com/ews/exchange.asmx",
        [Parameter(Mandatory = $false)][ValidateSet("ExchangeOnline", "ExchangeOnPrem")][string]$EndPoint = "ExchangeOnline",
        [Parameter(Mandatory = $true)][System.Management.Automation.PSCredential]$Credentials,
        [Parameter(Mandatory = $true)][int]$NumberOfItems,
        [Parameter(Mandatory = $true)][string[]]$Senders,
        [Parameter(Mandatory = $true)][string[]]$Recipients,
        [Parameter(Mandatory = $false)][int]$SubjectMinLength = 1,
        [Parameter(Mandatory = $false)][int]$SubjectMaxLength = 5,
        [Parameter(Mandatory = $false)][int]$BodyMinLines = 5,
        [Parameter(Mandatory = $false)][int]$BodyMaxLines = 50,
        [Parameter(Mandatory = $false)][int]$MaxRecipients = 5,
        [Parameter(Mandatory = $true)][string[]]$Dictionary,
        [Parameter(Mandatory = $true)][string[]]$Text,
        [Parameter(Mandatory = $false)][switch]$UploadAttachments = $false,
        [Parameter(Mandatory = $false)][string]$AttachmentsDirectory,
        [Parameter(Mandatory = $false)][switch]$CreateMeetingInvites = $false,
        [Parameter(Mandatory = $false)][int]$MeetingMinDays = 1,
        [Parameter(Mandatory = $false)][int]$MeetingMaxDays = 7,
        [Parameter(Mandatory = $false)][int]$MeetingMinHour = 8,
        [Parameter(Mandatory = $false)][int]$MeetingMaxHour = 18,
        [Parameter(Mandatory = $false)][string[]]$MeetingMinStart = ("0", "30"),
        [Parameter(Mandatory = $false)][string[]]$MeetingLength = ("30", "60", "90", "120", "180", "240"),
        [Parameter(Mandatory = $false)][int]$MeetingPercentage = 5
    )
       #Generates the email message body text and Subject line
 
    # Check that all of the required files are present
    $EWSDLL = "C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll"
    if (Test-Path $EWSDLL) {
        Import-Module $EWSDLL
    }
    else {
        Write-Host -ForegroundColor Yellow "Unable to locate Exchange Web Services DLL."
        break
    }
        
    #Web services initialization
    Write-Verbose "Preparing EWS"
    
    $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($SchemaType)
    $service.UseDefaultCredentials = $false
    $service.Credentials = New-Object System.Net.NetworkCredential($Credentials.UserName, $Credentials.Password)
    If ($UseAutoDiscover) {
        Write-Verbose "Using Autodiscover"
        Try {
            $service.AutodiscoverUrl($AutoDiscoverEmailAddress, {$true})
        }
        catch {
            Write-host "Unable to connect to Autodiscover"
            break
        }
    }
    Else {
        Write-verbose "Use Configured URL"
        $service.Url = $EWSURL
    }
    
    $SendCount = $NumberOfItems
    Write-verbose "Starting email generation loop"
    # Send all the emails
    For ($sent = 0; $sent -lt $sendcount; $sent++) {
        # Sends the email message via EWS with impersonation based on email subject, body, sender and recipient details that are randomly generated
        $Sender = Get-Random -InputObject $senders
        Write-Verbose "Sender: $Sender"
        $ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId -ArgumentList ([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SMTPAddress), $Sender
        $service.ImpersonatedUserId = $ImpersonatedUserId
    
        #Determine type of email to send
        #0-75 = New Message
        #76-95 = Reply
        #96-100 = Meeting Invite if enabled or Reply
        
        $msgtypeseed = (get-random -Minimum 0 -Maximum 100)
        if ($msgtypeseed -lt (100 - $MeetingPercentage) -or $CreateMeetingInvites -eq $false) {
            
            if ($msgtypeseed -le 75) {
                #Generate new Email
                Write-Verbose "New Message"
                $mail = New-Object Microsoft.Exchange.WebServices.Data.EmailMessage($service)
            }        
            else {
                write-Verbose "Reply to Message"
                #Retrieve items from the mailbox
                $ReplySource = 100
                $Folder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox)
                $itemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView($ReplySource, $skipped, [Microsoft.Exchange.Webservices.Data.OffsetBasePoint]::Beginning)
                $searchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::IsRead, $False)
                $findResults = $service.FindItems($folder.Id, $SearchFilter, $itemView);
                #Validate that there are valid items to reply - if not, create a new mail item
        
                #$ValidItems = ($findResults.Items| Where-Object {$_.from -notmatch $Sender})[0]
                $ValidItems = $findResults.Items| Where-Object {$_.from -notmatch $Sender}
                if ($ValidItems) {
                    $Replyitem = get-random -InputObject $ValidItems
                    #Check Item Class and respond accordingly
                    Switch ($Replyitem.ItemClass) {
                        "IPM.Schedule.Meeting.Request" {
                            #Respond to the meeting invite with an Accept or Decline
                            $responsetype = Get-Random -InputObject ("CreateAcceptMessage", "CreateDeclineMessage")
                            if ($responsetype -eq "CreateAcceptMessage") {
                                Write-Verbose "Responding to Meeting request with Accept"
                                $Mail = $Replyitem.CreateAcceptMessage([Boolean](get-Random -Minimum 0 -Maximum 2))
                            }
                            else {
                                $Mail = $Replyitem.CreateDeclineMessage()
                            }
                        }
                        "IPM.Note" {
                            #Set ReplyAll setting
                            [Boolean]$replyToAll = get-Random -Minimum 0 -Maximum 2
                            $Mail = $Replyitem.CreateReply($replyToAll);
                        }
                        default {
                            $mail = New-Object Microsoft.Exchange.WebServices.Data.EmailMessage($service)
                        }
                    }
                }
            }    
        }   
        else {
            #Generate Appointment
            $mail = New-Object Microsoft.Exchange.WebServices.Data.appointment($service)
            $mail.Start = get-date -Date (get-date).AddDays((Get-Random -Minimum $MeetingMinDays -Maximum $MeetingMaxDays)).Date.ToShortDateString()  -Hour (get-random -Minimum $MeetingMinHour -Maximum $MeetingMaxHour) -Minute (get-random -InputObject $MeetingMinStart)
            $mail.End = $mail.Start.AddMinutes((get-random -InputObject $MeetingLength))
            Write-verbose ("Start: {0}" -f $mail.Start)
            Write-verbose ("End: {0}" -f $mail.end)
        }


        $msgtype = $mail.GetType().Name
        switch ($msgtype) {
            {($_ -eq "AcceptMeetingInvitationMessage") -or ($_ -eq "DeclineMeetingInvitationMessage")} {
                #Should there be a response in the body
                IF ([Boolean](get-Random -Minimum 0 -Maximum 2)) {
                    #Add text to the body
                    $mail.Body = _GenerateText -Min 1 -Max 3 -Source $Text
                } 
            }
            "ResponseMessage" {
                $mail.BodyPrefix = _GenerateText -Min $BodyMinLines -Max $BodyMaxLines -Source $Text
            }
            {($_ -eq "EmailMessage") -or ($_ -eq "Appointment")} {
                #Add Subject and Body to the email/meeting
                Write-Verbose "Generating text for body of email"
                $mail.Body = _GenerateText -Min $BodyMinLines -Max $BodyMaxLines -Source $Text
                Write-verbose "Generating Subject Line"
                $mail.Subject = _GenerateText -Min $SubjectMinLength -Max $SubjectMaxLength -Source $Dictionary -ProperCase
                Write-verbose "Subject: $($mail.Subject)"
                #Add recipients as this is not a reply email
                #Choose recipients, and make sure recipients and sender don't match.
                #A random number of recipients will be added to the message.
                $tocount = Get-Random -Minimum 1 -Maximum ($MaxRecipients + 1)
                Write-Verbose "ToCount: $tocount"
                $recip = @()
                For ($i = 0; $i -lt $tocount; $i++) {
                    $recip += Get-Random -InputObject ($recipients| Where-Object {$_ -ne $Sender})
                }
                if ($msgtype -eq "EmailMessage") {
                    #Add to Torecipients
                    foreach ($r in ($recip|Select-Object -unique)) {
                        $mail.ToRecipients.Add($r) | out-null
                        Write-verbose "Recipient: $r"
                    }
                }
                else {
                    #Add to RequiredAttendees
                    foreach ($r in ($recip|Select-Object -unique)) {
                        $mail.RequiredAttendees.Add($r) |out-null
                        Write-verbose "Recipient: $r"
                    }
                    [Void] $mail.RequiredAttendees.Add($r)
                }
            }
        }
        if ($UploadAttachments -eq $true -and $AttachmentsDirectory) {
            #attempt to add attachment to file
            $rand = Get-Random -Minimum 0 -Maximum 10
            if ($rand -gt 7) {
                $files = @(Get-ChildItem $AttachmentsDirectory| Where-Object { ! $_.PSIsContainer })
                $file = Get-Random -InputObject $files
            }
            if ($file) {
                #Add the attachment
                Write-verbose "Attachment: $($File.FullName)"
                $mail.Attachments.AddFileAttachment("$($File.FullName)") | out-null
            }
        }
        if ($msgtype -eq "EmailMessage" -or $msgtype -eq "ResponseMessage") {
            #Send the message
            try {
                $mail.SendAndSaveCopy()
                #Output an object for display
                ""|Select-Object @{Name = "Sender"; Expression = {$Sender}}, @{Name = "ItemType"; Expression = {$msgType}}, @{Name = "EndPoint"; Expression = {$EndPoint}}
            }
            catch {}
        }
        else {
            try {
                $mail.Save()
                ""|Select-Object @{Name = "Sender"; Expression = {$Sender}}, @{Name = "ItemType"; Expression = {$msgType}}, @{Name = "EndPoint"; Expression = {$EndPoint}}
            }
            catch {}
        }
    }
    if ($Replyitem) {    
        #Mark Message As Read and update - prevents the same email from being replied to repeatedly
        $Replyitem.IsRead = $true
        $Replyitem.update("AutoResolve")
        $Replyitem = $null
    }
}

Function Get-DataGatheringFiles () {
    Invoke-Item (get-module EXOTools).FileList
}

function Get-RecipientInformation() {
    Param (
        [Parameter(Mandatory = $true)][string]$UserName,
        [Parameter(Mandatory = $false)][switch]$Statistics = $false,
        [Parameter(Mandatory = $false)][switch]$OOF = $false,
        [Parameter(Mandatory = $false)][switch]$JunkEmail = $false,
        [Parameter(Mandatory = $false)][switch]$MailboxPermissions = $false,
        [Parameter(Mandatory = $false)][switch]$CalendarProcessing = $false
    )

    $recips = get-Recipient -identity $username|Select-Object Name, Alias, PrimarySMTPAddress, OrganizationalUnit, City, CountryOrRegion, Office, Company, RecipientTypeDetails, Database, HiddenFromAddressListsEnabled

    foreach ($user in $recips) {
        Write-header "Recipient Information: $($user.Name)"
        $User|Format-List

        switch ($User.RecipientTypeDetails) {
            {$_ -match "Mailbox"} {
                if ($Statistics -eq $true) {
                    #Grab Mailbox Statistics
                    Write-Header "Mailbox Quotas"
                    get-mailbox $User.Alias|Select-Object Alias, UseDatabaseQuotaDefaults, IssueWarningQuota, ProhibitSendQuota, ProhibitSendReceiveQuota
                    Write-Header "Mailbox Statistics"
                    get-mailboxstatistics -identity $user.Alias|Select-Object ItemCount, TotalItemSize, StorageLimitStatus, TotalDeletedItemSize, LastLogonTime, LastLoggedOnUserAccount
    
                    #Grab Folder Statistics
                    Write-Header "Folder Statistics"
                    get-mailboxfolderstatistics $user.alias|Format-Table -AutoSize FolderPath, FolderType, ItemsInFolder
                }
                if ($OOF -eq $true) {
                    #Grab OOF Configuration
                    Write-Header "OOF Configuration"
                    $OOFConfig = get-mailboxAutoReplyConfiguration $user.alias
                    if ($OOFConfig.AutoReplyState -notmatch "Disable") {$OOFConfig |Select-Object AutoReplyState, StartTime, EndTime, InternalMessage, ExternalAudience, ExternalMessage}
                    else {$OOFConfig|Select-Object AutoReplyState}
                }
                if ($JunkEmail -eq $true) {
                    #Grab Junk Email Settings
                    Write-Header "Junk E-Mail Configuration"
                    Get-MailboxJunkEmailConfiguration $User.Alias | Select-Object * -Exclude RunSpaceID, MailboxOwnerID, Identity, IsValid, ObjectState
                }
                if ($MailboxPermissions -eq $true) {
                    #Grab AD Permission for FullAccess and SendAs
                    Write-Header "Mailbox Permissions"
                    get-mailboxpermission $user.Alias|Where-Object {($_.AccessRights -match "FullAccess") -and $_.Deny -ne $true}|Select-Object @{Expression = {$_.User}; Name = "FullAccess"} | Format-Table
                    #$Perms|Where-Object {$_.AccessRights -match "FullAccess"}
                    #$Perms|Where-Object {$_.AccessRights -match "SendAS"}|Select-Object @{Expression = {$_.User}; Name = "SendAs"}
                }
                Write-Header "Mailbox Features"
                #CAS Mailbox
                $CASMBX = Get-CASMailbox -Identity $User.Alias
                $CASMBX|Select-Object *Enabled
                #ActiveSync Device
                if ($CASMBX.HasActiveSyncDevicePartnership -eq $true) {
                    Write-Header "ActiveSync Device Statistics"
                    get-activesyncdevicestatistics -mailbox $user.alias|Format-List DeviceFriendlyName, DeviceOS, DeviceModel, Status, StatusNote, LastSyncAttemptTime, LastSuccessSync
                }
            }
            {$_ -match "DistributionGroup"} {
                if ($User.RecipientTypeDetails -eq "DynamicDistributionGroup") {$DL = Get-DynamicDistributionGroup -identity $User.alias}
                else {$DL = Get-DistributionGroup -identity $User.alias}
    
                Write-Header "Distribution Group Settings"
                $DL|Format-List PrimarySMTPAddress, RecipientTypeDetails, OrganizationalUnit, WhenCreatedUTC, WhenChangedUTC, ManagedBy, AcceptMessagesOnlyFrom
    
            }
            #MailUser
            "MailUser" {
                Write-Header "Mail User"
                get-mailuser $user.alias | Format-List PrimarySMTPAddress, ExternalEmailAddress
            }

            #Contact
            "MailContact" {
                Write-Header "Mail Contact"
                get-contact $user.alias|Format-List WindowsEmailAddress
            }    
        }
    }
}
Function Get-BlockedSenderInfo () {
<#
 .Synopsis
 Generate message traces for blocked senders in Exchange Online
 .Description
 Queries the Blocked Senders list in Exchange Online Admin Center and performs a message trace for each user for the last 24 hours
  .Parameter Days
  Number of days ago to search for blocked users
 .Parameter IncludeMessageTraceDetail
  Perform a Get-MessageTraceDetail instead of just Get-MessageTrace
 .Parameter RemoveBlock
  Automatically attempt to remove the blocked sender
  .Example
    Get-BlockedSenderInfo -Days 14
    Query the blocked senders list for addresses that have been blocked in the last 14 days
  .Example
    Get-BlockedSenderInfo -IncludeMessageTraceDetail
    Run Get-MessageTraceDetail for all blocked senders
  .Example
    Get-BlockedSenderInfo -RemoveBlock
    Run Get-MessageTraceDetail for all blocked senders and then attempt to remove them from the blocked list
    #>


    Param (
    [int]$Days = 7,
    [switch]$IncludeMessageTraceDetail,
    [switch]$RemoveBlock = $false
    )
    if (!(get-command Get-BlockedSenderAddress -ErrorAction SilentlyContinue) ) {
        Write-host "Not connected to Exchange Online" -ForegroundColor Red
        break
    }
    $blockedusers = Get-BlockedSenderAddress | Where-Object {$_.CreatedDatetime -gt (get-date).AddDays($Days * -1)}
    
    foreach ($user in $blockedusers) {
    
    $Name = $($user.SenderAddress.Split("@")[0])
    
    Write-Host "Found Blocked Sender: $($user.SenderAddress) : $($user.CreatedDateTime)"
    #Run Get-MessageTrace for anything newer than 7 days
        if ($user.CreatedDateTime -gt (get-date).AddDays(-7)) {
        $results = Get-MessageTrace -SenderAddress $user.SenderAddress -StartDate ($user.CreatedDatetime.AddHours(-24)) -EndDate $user.CreatedDatetime -PageSize 5000
        if ($IncludeMessageTraceDetail -eq $true) {
    
            $results = $results | Get-MessageTraceDetail
    
        }
        Write-Host "Exporting results to $($name).csv"
        $results| Export-Csv -NoTypeInformation -Path "$($name).csv"
        }
        else {
            #Run Start-HistoricalSearch for anything over 7 days
            $results = Start-HistoricalSearch -SenderAddress $user.SenderAddress -StartDate ($user.CreatedDatetime.AddHours(-24)) -EndDate $user.CreatedDatetime -ReportType MessageTrace -ReportTitle "Blocked Sender - $name"
            Write-Host "Started Historical Search: `nSubmit Date: $($results.SubmitDate) `nJobID: $($results.JobId.Guid) `nReport Title: $($results.ReportTitle)"
            Write-Host "Please check the Message Trace section in Exchange Online for information on the status of the Historical search"
        }
        if ($RemoveBlock -eq $true) {
            Write-Host "Removing $($user.SenderAddress) from the Blocked Senders list"
            Remove-BlockedSenderAddress -SenderAddress $user.SenderAddress
        }
        Write-Host $delim
    }
}

Function Start-MRMMonitor () {
        <#
 .Synopsis
 Monitors Mailbox Statistics for Primary and any Archive Mailboxes.
  
 .Description
 Useful for monitoring the Managed Folder Assistant progress in moving items from the Primary Mailbox to the Archive Mailbox
 
 .Parameter Identity
 The identity of the mailbox (UserPrincipalName, PrimarySMTPAddress, Alias, etc)
 
 .Parameter Interval
 Number of seconds to wait before executing the next run
 
 .Parameter Count
 Number of runs to execute
 
 .Parameter IncludeDiagnostics
 Run the Export-MailboxDiagnosticLogs for MRM Component and the ExtendedProperties
 
 .Parameter OutputFile
 File path to export the mailbox statistics for advanced tracking
 
 .Example
 Start-MRMMonitor -identity user@contoso.com
 Monitor user@contoso.com's mailbox statistics with the default values
 
 .Example
 Start-MRMMonitor -identity user@contoso.com -Interval 60 -Count 100
 Wait 60 seconds between each run for a total of 100 runs.
 
 .Example
 Start-MRMMonitor -identity user@contoso.com -OutputFile C:\Temp\user.csv
 Monitor user@contoso.com's mailbox statistics with the default values
 Output the statistics to a CSV file named C:\Temp\user.csv
 
 .Example
 Start-MRMMonitor -identity user@contoso.com -IncludeDiagnostics
 Monitor user@contoso.com's mailbox statistics with the default values
 Include the Export-MailboxDiagnosticLogs for the user
 
#>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)][string]$Identity,
        [Parameter(Mandatory = $false)][int]$Interval = 300,
        [Parameter(Mandatory = $false)][int]$Count = 300,
        [Parameter(Mandatory = $false)][switch]$IncludeDiagnostics,
        [Parameter(Mandatory = $false)][switch]$SkipFolderStatistics,
        [Parameter(Mandatory = $false)][string]$OutputFile
    )
    $i=0
    $fldrstats=@()
    $mbxstats=@()
    $htDI = @{} #Deleted Item Count Hash Table
    $htIC = @{} #Item Count Hash Table
    
    Write-Verbose "Mailbox to monitor: $($Identity)"
    Write-Verbose "Number of times: $($Count)"
    Write-Verbose "Interval between queries: $($Interval)"
    Write-Verbose "Querying for mailbox"
    $mbx = get-mailbox -Identity $identity -ErrorAction SilentlyContinue
    if ($mbx) {
        While ($i -lt $count) {
            $pts = $ts
            $ts = get-date
            Write-Verbose "Run time: $($ts)"
            if (!$SkipFolderStatistics) {
                Write-Verbose "Gathering mailbox folder statistics for Deleted Items and Recoverable Items"
                $fldrstats += Get-MailboxFolderStatistics -Identity $Identity -FolderScope DeletedItems -IncludeOldestAndNewestItems | Select-Object  @{n="TimeStamp";e={$ts}},FolderPath, ItemsInFolder, FolderSize, OldestItemReceivedDate
                $fldrstats += Get-MailboxFolderStatistics -Identity $Identity -FolderScope RecoverableItems -IncludeOldestAndNewestItems | Select-Object  @{n="TimeStamp";e={$ts}},FolderPath, ItemsInFolder, FolderSize, OldestItemReceivedDate
                }
            Write-Verbose "Gathering mailbox statistics"
            #Mailbox statistics
            $mbxstats +=_mbxloc -Mailbox $mbx -timestamp $ts
            if (!$SkipFolderStatistics) {
                Write-Header "Folder Statistics"
                $fldrstats | Where-Object {$_.TimeStamp -eq $ts} | Format-Table -AutoSize
            }
            Write-Header "Mailbox Statistics"
            $mbxstats| Where-Object {$_.TimeStamp -eq $ts} | ForEach-Object {
                $_ | Select-Object Type,TotalItemSize, ItemCount, TotalDeletedItemSize,DeletedItemCount | Format-Table -AutoSize
                #Calculate Deltas
                $guid = $_.GUID.Tostring()
                if ($i -gt 0) {
                    Write-Header "Change in items - Last Run $([int]($ts - $pts).TotalSeconds) seconds ago"
                    $_| Select-Object Type, @{n="ItemCountDelta";e={$_.ItemCount-$htic[$guid]}}, @{n="DeletedItemCountDelta";e={$_.DeletedItemCount-$htdi[$guid]}} | Format-Table -AutoSize
                }
                #Store previous values in hash tables
                if ($htIC.ContainsKey($guid)) {$htic[$guid]=$_.ItemCount} else {$htIC.add($guid, $_.ItemCount)}
                if ($htDI.ContainsKey($guid)) {$htDI[$guid] = $_.DeletedItemCount} else {$htDI.add($guid, $_.DeletedItemCount)}
            }
            if ($IncludeDiagnostics) {
                Write-Verbose "Gathering MRM and Extended Properties diagnostics logs"
                #MRM Diagnostics & Extended Properties
                Write-Header "Diagnostic Logs"
                (Export-MailboxDiagnosticLogs -ComponentName MRM $Identity).Mailboxlog
                [xml]$ep = (Export-MailboxDiagnosticLogs -ExtendedProperties $Identity).Mailboxlog
                $ep.Properties.MailboxTable.Property| Where-Object {$_.Name -match "ELC"} | Format-Table -AutoSize
            }
            $i++
            if ($OutputFile) {
                Write-Verbose "Exporting current object to $($OutputFile)"
                $mbxstats | Export-Csv -Path $outputfile -NoTypeInformation
            }
            if ($i -lt $count) {
                Write-Verbose "Sleeping $($interval) seconds"
                Start-Sleep -Seconds $interval
            }
        }
    }
}
Function Get-PotentialAutoExpandingArchiveUsers () {
    <#
     .Synopsis
     Analyze specified users for AutoExpanding Archives.
      
     .Description
        This is a proactive measure to determine which users may benefit from Auto-Expanding Archives being enabled
        Queries the system for Litigation Hold and InPlace Holds for mailboxes as well as the mailbox statistics for the Primary and Archive mailboxes (if applicable).
        Provides recommendations on enabling Auto-Expanding Archives for applicable users as well as generating warnings if a user is above the Recoverable Items Warning Quotas.
     
     .Parameter Users
     The identity of the mailbox(es) (UserPrincipalName, PrimarySMTPAddress, Alias, etc).
     This can be a single mailbox, a list of mailboxes or a collection of mailboxes from Get-Mailbox
     
     .Parameter MicroDelay
     Number of milliseconds to wait before executing the next run. Helps with PowerShell throttling
     
     .Example
     Get-PotentialAutoExpandingArchiveUsers -Users user@contoso.com
     Analyze the provided user
     
     .Example
     Get-PotentialAutoExpandingArchiveUsers -Users user@contoso.com,user2@contoso.com
     Analyze the two provided users
     
     .Example
     Get-PotentialAutoExpandingArchiveUsers -Users (Get-mailbox -recipientTypeDetails UserMailbox)
     Analyze the first 1000 mailboxes returned with the Get-Mailbox command
     
    #>

        [CmdletBinding()]
        Param (
            $Users,
            [int]$MicroDelay = 0
        )
        
        Write-Verbose "Creating table"
        $table = New-Object system.Data.DataTable “PotentialUsers”
        
        #Add the Columns
        Write-Verbose "Adding columns to table"
        [void]$table.columns.add("Name",[string])
        [void]$table.columns.add("UserPrincipalName",[string])
        [void]$table.columns.add("ExchangeGUID",[string])
        [void]$table.columns.add("ArchiveGUID",[string])
        [void]$table.columns.add("MailboxLocations",[string])
        [void]$table.columns.add("AutoExpandingArchiveEnabled",[Boolean])
        [void]$table.columns.add("LitigationHoldEnabled",[Boolean])
        [void]$table.columns.add("InPlaceHoldsEnabled",[Boolean])
        [void]$table.columns.add("ItemCount",[int])
        [void]$table.columns.add("DeletedItemCount",[int])
        [void]$table.columns.add("TotalItemSize",[string])
        [void]$table.columns.add("TotalRecoverableItemSize",[string])
        [void]$table.columns.add("RecoverableItemsWarning",[Boolean])
        [void]$table.columns.add("ArchiveItemCount",[int])
        [void]$table.columns.add("ArchiveDeletedItemCount",[int])
        [void]$table.columns.add("ArchiveTotalItemSize",[string])
        [void]$table.columns.add("ArchiveTotalRecoverableItemSize",[string])
        [void]$table.columns.add("ArchiveRecoverableItemsWarning",[Boolean])
        [void]$table.columns.add("Recommendation",[string])
        $table.Columns["RecoverableItemsWarning"].DefaultValue = $false
        $table.Columns["ArchiveRecoverableItemsWarning"].DefaultValue = $false
        
        
        #Check Mailbox Statistics
        foreach ($user in $Users) {
        
            if ($user.UserPrincipalName -eq $null) {
                Write-Verbose "User: $($user) is not a mailbox - running get-mailbox to get the correct object"
                $un = $user
                $user = get-Mailbox $user -ResultSize 1 -WarningAction SilentlyContinue -erroraction SilentlyContinue
                }
                if ($user) {
                    Write-Verbose "Checking $($row.userPrincipalName)"
                    $RIWarning = $user.RecoverableItemsWarningQuota -replace "(.*\()|,| [a-z]*\)", ""
                    Write-Verbose "Recoverable Items Warning Quota: $($user.RecoverableItemsWarningQuota)"
                    $row = $table.NewRow()
                    $row.Name = $user.Name
                    $row.UserPrincipalName = $user.UserPrincipalName
                    $row.ExchangeGUID = $user.ExchangeGuid.Guid
                    $row.ArchiveGUID = $user.ArchiveGuid.Guid
                    $row.MailboxLocations = $user.MailboxLocations
                    $row.AutoExpandingArchiveEnabled = $user.AutoExpandingArchiveEnabled
                    $row.LitigationHoldEnabled = $user.LitigationHoldEnabled
                    $row.InPlaceHoldsEnabled = ($user.InPlaceHolds.count -gt 0)
                    if ($row.AutoExpandingArchiveEnabled) {Write-Warning "AutoExpanding Archive already enabled for user $($row.UserPrincipalName)"}
                    Write-Verbose "Querying mailbox statistics for $($row.UserPrincipalName) : Primary : $($row.ExchangeGUID)"
                    $stats = Get-MailboxStatistics -Identity $row.ExchangeGUID| Select-Object ItemCount, DeletedItemCount, @{n="TotalItemSizeBytes";e={$_.TotalItemSize -replace "(.*\()|,| [a-z]*\)", ""}},@{n="TotalDeletedItemSizeBytes";e={$_.TotalDeletedItemSize -replace "(.*\()|,| [a-z]*\)", ""}},TotalDeletedItemSize
                    $row.ItemCount = $stats.ItemCount
                    $row.DeletedItemCount = $stats.DeletedItemCount
                    $row.TotalItemSize = $stats.TotalItemSizeBytes
                    $row.TotalRecoverableItemSize = $stats.TotalDeletedItemSizeBytes
                    Write-Verbose "Recoverable Items: $($stats.TotalDeletedItemSize)"
                    if ($stats.TotalDeletedItemSizeBytes -ge $RIWarning) {
                        Write-Warning "Primary: Recoverable Items is above the warning quota: $($row.UserPrincipalName)"
                        $row.RecoverableItemsWarning = $true
                    }
            
                    If ($user.ArchiveGuid -ne "00000000-0000-0000-0000-000000000000") {
                        Write-Verbose "Querying mailbox statistics for $($row.UserPrincipalName) : Archive : $($row.ArchiveGUID)"
                        $stats = Get-MailboxStatistics -Identity $row.ArchiveGUID| Select-Object ItemCount, DeletedItemCount, @{n="TotalItemSizeBytes";e={$_.TotalItemSize -replace "(.*\()|,| [a-z]*\)", ""}},@{n="TotalDeletedItemSizeBytes";e={$_.TotalDeletedItemSize -replace "(.*\()|,| [a-z]*\)", ""}},TotalDeletedItemSize
                        $row.ArchiveItemCount = $stats.ItemCount
                        $row.ArchiveDeletedItemCount = $stats.DeletedItemCount
                        $row.ArchiveTotalItemSize = $stats.TotalItemSizeBytes
                        $row.ArchiveTotalRecoverableItemSize = $stats.TotalDeletedItemSizeBytes
                        Write-Verbose "Recoverable Items: $($stats.TotalDeletedItemSize)"
                        if ($stats.TotalDeletedItemSizeBytes  -ge $RIWarning) {
                            Write-Warning "Archive: Recoverable Items is above the warning quota: $($row.UserPrincipalName)"
                            $row.ArchiveRecoverableItemsWarning = $true
                        }
                    }
                    if (($row.LitigationHoldEnabled -eq $true -or $row.InplaceHoldsEnabled -eq $true) -and $row.AutoExpandingArchiveEnabled -eq $false) { 
                        #No Archive Enabled
                        if ($row.ArchiveGUID -eq "00000000-0000-0000-0000-000000000000") {
                            $row.Recommendation = "Enable Archive and Auto-Expanding Archive"
                        }
                        else {
                            #Archive already enabled
                            $row.Recommendation = "Enable Auto-Expanding Archive"
                        }
                    }
                    else {$row.Recommendation = "None"}
                    $table.Rows.Add($row)
                }
                else {
                    Write-Warning "Could not find a mailbox for $($un)"
                }
                if ($MicroDelay -gt 0) {
                    Write-Verbose "Sleeping $($MicroDelay) ms to avoid powershell throttling"
                    Start-Sleep -Milliseconds $MicroDelay
                }
            }
            return $table.rows
        }
    
function _mbxloc () {
<#
    Internal function to grab mailbox statistics from mailbox locations
#>

[CmdletBinding()]
    Param (
        $Mailbox,
        [datetime]$TimeStamp = (get-date)
    )
    Write-Verbose "Querying mailbox statistics for $($Mailbox.UserPrincipalName)"
    Foreach ($m in $mailbox.MailboxLocations) {
        Write-Verbose "$($m)"
        Get-MailboxStatistics -Identity $m.Split(";")[1]| Select-Object @{n="TimeStamp";e={$TimeStamp}}, @{n="Type";e={$m.Split(";")[2]}}, @{n="GUID";e={$_.Identity}},ItemCount, DeletedItemCount, TotalItemSize, @{n="TotalItemSizeBytes";e={$_.TotalItemSize -replace "(.*\()|,| [a-z]*\)", ""}},TotalDeletedItemSize,@{n="TotalDeletedItemSizeBytes";e={$_.TotalDeletedItemSize -replace "(.*\()|,| [a-z]*\)", ""}}
    }
}

Function Set-Mailbox1 () {
    Param (
        [String]$Identity
    )

    Set-Mailbox -Identity $Identity -LitigationHoldEnabled $false -SingleItemRecoveryEnabled $false -RetainDeletedItemsFor 0 -ElcProcessingDisabled $true -RemoveDelayHoldApplied
    #Disable MFA, Single Item Recovery and Litigation hold
    Set-Mailbox -Identity $identity -ElcProcessingDisabled $true
    Set-Mailbox -Identity $identity -SingleItemRecoveryEnabled $true
}

Function Submit-SafeLinksFalseNegative () {

    <#
    .SYNOPSIS
    Drafts and displays an email to submit a false negative to the SafeLinksFeedback Team.
    .DESCRIPTION
    This cmdlet takes a bad URL that you wish to block and the reference as input. It creates and opens a draft mail in Outlook, which you can review and send to the Safe Links analysts for processing. The link specified with the -URL parameter MUST begin with http:// or https:// in order to be parsed correctly.
    .EXAMPLE
    Submit-ATPSLFN -URL "https://contoso.com/ww/www/" -Reference 12345
    .PARAMETER URL
    The URL that you wish to report as a false-negative.
    .PARAMETER Reference
    A reference that is associated with this request.
    #>

    Param (
        [parameter(Mandatory=$True)][ValidatePattern(".*\..*")][String]$URL,
        [parameter(Mandatory=$True)][String]$Reference
        )
       #Credit to Andy Day for the original source
  # Rewrite URL
  $NewUrl=$URL.Replace(".","[dot]")
  
  $timestamp= Get-Date -Format "yyMMdd HH:mm:ss"
  Try {
    # Compile message 1 - in Outlook and save/open as a Draft
    $ol = New-Object -comObject Outlook.Application
    
    $mail = $ol.CreateItem(0)
    $mail.To = "SafeLinksFeedback@microsoft.com"
    $mail.Subject = "[Potential Malicious URL Submission] $timestamp [$Reference]"
    $mail.Body = "Please add this URL to the list: $NewUrl"
    $mail.HTMLBody = "Please add this URL to the list: <br>$NewUrl<br><br>"

    
    $inspector = $mail.GetInspector
    $inspector.Display()
  }
  Catch {
      Write-host "Unable to create new email in Outlook"
  }
}
Function Get-MSOLValidationErrors () {

    Param(
        [string]$SearchString = ""
    )
    $users = get-msoluser -HasErrorsOnly -SearchString $SearchString
    Foreach ($user in $users) {
        Write-Header "User: $($user.UserPrincipalName)"
        Write-Output $user[0].errors.errordetail.objecterrors.ErrorRecord.ErrorDescription
    }
}

Function Compare-DistributionGroup () {
    
    <#
    .SYNOPSIS
    Compares membership between Azure AD and Exchange Online groups
    .DESCRIPTION
    Queries the group membership from Azure AD (Get-MSOLGroup) and from Exchange Online (get-group) to ensure that the membership count is correct.
    .EXAMPLE
    Compare-DistributionGroup -GroupName ContosoUsers
    .PARAMETER GroupName
    Group Name or other identity to use to query for group information
    #>

    
    Param (
        [parameter(Mandatory=$True)][string]$GroupName
    )
    Write-Header "Checking Membership: $($GroupName)"
    $DL = get-distributionGroup -identity $GroupName -ErrorAction SilentlyContinue -ResultSize 1 -WarningAction SilentlyContinue

    if ($DL) {
        $DLMembers = @(get-distributiongroupmember -Identity $dl.identity)
        $EXOGroup = @(get-group -identity $dl.identity)
        $AADGroup = @(Get-MsolGroupMember -GroupObjectId $DL.ExternalDirectoryObjectID)
    }    
    else {
        Write-Header "Group $($GroupName) not found"
    }
    If ($DLMembers.count -ne $AADGroup.Count ) {
        Write-host "Groups are out of sync"
        Write-Host "EXO Group Member Count $($EXOGroup.Members.count)"
        Write-Host "EXO Distribution Group Member Count $($DLMembers.count)"
        Write-Host "Azure AD Group Member Count $($AADGroup.Count)"
    }    
    else { 
        Write-Host "Member Counts are in sync"
    }

    $Results = Compare-Object -ReferenceObject ($aadgroup.displayname| Sort-Object) -DifferenceObject ($DLMembers.displayname | Sort-Object)
    
    $AADDiff = ($results | Where-Object {$_.SideIndicator -eq "<="}).InputObject
    if ($AADDiff) {
        Write-Header "Members that exist in Azure AD and not Exchange Online"
        $AAFDiff
    }

    $EXODiff = ($results | Where-Object {$_.SideIndicator -eq "=>"}).InputObject
    if ($EXODiff) {
        Write-Header "Members that exist in Exchange Online and not Azure AD"
        $EXODiff
    } 
}