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