Exchange_AddIn.psm1
<# PowerShell Module 'Exchange_AddIn' Mike O'Neill, Microsoft Senior Customer Engineer https://mikeoneill.blog/ LEGAL DISCLAIMER The sample scripts are not supported under any Microsoft standard support program or service. The sample scripts are provided AS IS without warranty of any kind. Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the sample scripts or documentation, even if Microsoft has been advised of the possibility of such damages. #> <# .SYNOPSIS Compilation of scripts and functions to help administrate Exchange on premises, Exchange online, Exchange hybrid, and Active Directory environments. .DESCRIPTION This is a compilation of scripts and functions packaged into a single module with verb-noun cmdlets for engineers to easily run and rerun as needed. .NOTES History: 2.0.5 - February 15, 2023 - Fixed try/catch for EXOv2/v3 module for Get-EaiMailboxLocations, Get-EaiO365Photos, and Remove-EaiMobileStaleDevices 2.0.4 - July 13, 2022 - Updated Get-EaiMailboxLocations to include regional data and try/catch to use EXOv2 PS module. 2.0.3 - February 11, 2022 - Get-EaiCalendarEventsOfRecurrenceItems, adding more attributes for function 2.0.2 - November 4, 2021 - Get-EaiCalendarEventsOfRecurrenceItems updated logic for owner only listed events to be fixed. 2.0.1 - minor updates/fixes October 13, 2021 2.0 - Updated October 13, 2021 - Added module prefix of 'Eai' to functions, for 'Exchange_AddIn'. This follows best practices function naming in custom modules - Added Remove-EaiMobileStaleDevices function 1.9.8.5: Updated August 30, 2021 - Added Get-EAICalendarEventsOfRecurrenceItems - Added array of input for multiple end user scanning 1.9.8.3 Updated March 21, 2021 - Fixed PS 7+ issue for Get-EAIO365Photos - Changed error checking logic for logon and identity requirement 1.9.8.1 and 1.9.8.2 - Updated March 20, 2021 - Added individual option for users Get-EAIO365Photos - Fixed multiple individual input loop for Get-EAIO365Photos 1.9.8 - Updated 3/19/2021 - Added Get-EAIO365Photos for downloading of photos from O365 1.9.7 - Updated 3/3/2021 - Attempted to work with Get-HAFNIUM content. Did not need it due to other more updated resources online. 1.9.5 - Updated 5/8/2020 - Fixed missing allowed Export-CSVAllADSites function in Functions to Export - Reverted back to Exchange_AddIn module for users to know what the module is used for. Too much unknown for what MO_Module was. - Updated all helpURI to Exchange_AddIn vs. MO_Module - Launched all websites for help topics in Exchange_AddIn module 1.9.4 - Updated 3/31/2020 - Added Set-EAEMailAddressesForUsers function - Starting to use 'EA' as a prefix for functions in this module. 1.9.3 - Updated 3/17/2020 - Added Export-CSVAllADSites function - allowed variable parameter in Set-AutoDiscoverSiteScopeExchangeServers 1.9.2 - Updated 9/12/2019 - Updated Get-DOTNETVersion function 1.9.1 - Updated 9/12/2019 - Set-AutoDiscoverSiteScopeExchangeServers validation testing 1.9.0 - Updated 9/12/2019 - Reverted back to Exchange_AddIn module for users to know what the module is used for. Too much unknown for what MO_Module was. - Beta testing before full 2.0 roll out. Changing all of the names to Exchange_AddIn module for help URI. - Testing internally with functions for larger organizations. - Added function: Get-ExchangeServerDBWhiteSpace 1.2.5 - Updated 8/27/2019 - Added additional helpuri values to several functions - Corrected example information on several functions - Modified Connect-ExchangeOnline to be Connect-ExchangeOnlineNonMFA. Exchange PG is about to release Connect-ExchangeOnline in new REST API module with MFA available. 1.2.4 - Updated 7/29/2019 - Removed mandatory=$false from connection functions - added Get-MailboxLocations as a function - Added helpuri to functions. https://mikeoneill.blog/MO_Module is online location with sub folders for each function - Added 3rd example for connection functions - Updated help topics grammar in functions for greater clarity 1.2.3 - Updated 6/26/2019 - Fixed confirm information when connecting to on premises Exchange Server - Ensured functions worked on Windows Server 2019 - Ensured functions worked aganist Exchange Server 2019 - Corrected grammar for functions - Confirmed functions of working status and troubleshooting of processes 1.2.2 - Updated 6/25/2019 - Added parameter help content to functions - Fixed many PS Script Analyzer problems that were identified - Added in functions - Update-SpecificDLCSVImport - Update-SpecificDLTXTImport - Get-EXOEmailSecuritySettings - Move-MailQueDatabase - Changed prefix values in logon functions to be just -prefix. Should help make the parameter value easier to type and understand when using those functions. - Updated Get-MailboxAutoReplyConfigurationDomain function including comment block with setting a value to test in organization. - Added confirmation display when connecting to an on premises Exchange Server 1.2 - Updated 6/16/2019 - Updates Changed title of module to a more generic name: MO_Module - Adding both Exchange and Active Directory functions into one module - Some functions are generic for operating systems - Add Write-Verbose to functions missing the feature 1.1 - Posted 7/7/2017 - Fixes: - removed many 'Global' scopes in parameters in functions - fixed duplicate parameter entries in functions 1.0 - Posted 5/21/2017 - First iteration .FUNCTIONALITY This module assists Exchange, Active Directory, and Azure engineers with a single point of reference with useful cmdlets in a PowerShell module. #> #region Connect to Exchange on-premises server Function Request-CredentialExchangeOnPremises { # Request for new credentials function <# .SYNOPSIS Prompts user for a new user name and password for logon for onpremises server. .DESCRIPTION This cmdlet prompts a user to enter in a new user name and password to correct any errors or change logons to a server on premises. .EXAMPLE Request-CredentialOnPremises This function was built to re-enter on premises credentials so as not to have to close the PS session to flush the variable. .INPUTS Requests users' credentials .FUNCTIONALITY Prompts for new on premises credentials. #> [cmdletbinding()] Param() &$Global:OnPremisesUserCredential } # End Request for new onpremises credentials function #region Enter Exchange onpremises Credentials to log onto onpremises server $Global:OnPremisesUserCredential = { $Global:OnPremisesCredential = Get-Credential -Message "Your Sign-In is your Domain\UserName." } #endregion Enter Credentials to log into on premises servers. Function Connect-EaiExchangeServer { <# .SYNOPSIS Connects to a designated Exchange in the computer parameter .DESCRIPTION Screen output of current status of DAG environment. Lists current Databases, what the status of each database is, and how many copies are available for each Database. .EXAMPLE Connect-EaiExchangeServer -Computer MBX01 To connect up to an Exchange server on named MBX01 and not use cmdlet prefix option. .EXAMPLE Connect-EaiExchangeServer -Computer MBX04 -Prefix OnPrem This connects to Exchange server MBX04, and adds the prefix 'OnPrem' to all nouns in the remote PowerShell session. .EXAMPLE Connect-EaiExchangeServer -Computer EX12 -Prefix OP This connects to Exchange server EX12, and adds the prefix 'OP' to all nouns in the remote PowerShell session. .ROLE Exchange servers. .FUNCTIONALITY Connects remotely to an on premises Exchange server. #> [cmdletbinding(HelpUri = 'https://mikeoneill.blog/Exchange_AddIn/Connect-eaiExchangeServer')] param ([parameter(Mandatory=$true, HelpMessage="Enter a single computer name to create a remote PowerShell session.")] [string]$Computer, [parameter(HelpMessage="This is an optional parameter to add a prefix value to the Nouns in the remote PowerShell session to separate from other imported PowerShell sessions in the same PowerShell run space.")] [string]$Prefix = $Null ) If ($null -eq $OnPremisesCredential) { &$Global:OnPremisesUserCredential } If ($Null -eq $OnPremisesPrefix) { $Global:OnPremisesSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://$Computer/PowerShell/" -Authentication Kerberos -Credential $OnPremisesCredential Import-Module (Import-PSSession $OnPremisesSession -AllowClobber -DisableNameChecking ) -Global -DisableNameChecking Write-Verbose "`$Computer value is $Computer" $ConnectedExchangeServer = Get-ExchangeServer -Identity $Computer Write-Verbose ($ConnectedExchangeServer).name if ($null -ne $ConnectedExchangeServer) { Write-Host "" Write-Host "You are current logged onto the Exchange server: $ConnectedExchangeServer." -ForegroundColor Green Write-Host "" } else { Write-Host "" Write-Host "You are NOT current logged onto the Exchange server: $ConnectedExchangeServer." -ForegroundColor Red Write-Host "" } } Else { $Global:OnPremisesSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://$Computer/PowerShell/" -Authentication Kerberos -Credential $OnPremisesCredential Import-Module (Import-PSSession $OnPremisesSession -AllowClobber -DisableNameChecking ) -Global -DisableNameChecking -Prefix $OnPremisesPrefix Write-Verbose "`$OnPremisesComputer value is $Computer" Write-Verbose "`$Prefix value is $Prefix" $ConnectedExchangeServer = Get-ExchangeServer -Identity $Computer Write-Verbose ($ConnectedExchangeServer).name if ($null -ne $ConnectedExchangeServer) { Write-Host "" Write-Host "You are currently logged onto the Exchange server: $ConnectedExchangeServer, with remote PS prefix of: $Prefix." -ForegroundColor Green Write-Host "" } else { Write-Host "" Write-Host "You are NOT currently logged onto the Exchange server: $ConnectedExchangeServer." -ForegroundColor Red Write-Host "" } } } #endregion Connect to Exchange on-premises server #region Exchange Online sign in Function Request-CredentialExchangeOnlineNonMFA { <# .SYNOPSIS Prompts user for a new user name and password for logon to tenant. .DESCRIPTION This cmdlet prompts a user to enter in a new user name and password to correct any errors or change logons to an O365 tenant. .EXAMPLE Request-CredentialExchangeOnlineNonMFA This function was built to re-enter O365 credentials so as not to have to close the PS session to flush the variable. .FUNCTIONALITY Prompts for new O365 credentials that log onto Exchange online. #> [cmdletbinding()] param() &$Global:EXOUserCredential } # End Request for new credentials function #region Enter Exchange Online Credentials to log onto tenant $Global:EXOUserCredential = { $Global:EXOCredential = Get-Credential -Message "Your Sign-In is your e-mail address for O365." } #endregion Enter Credentials to log onto tenant #region Exchange Online connect/disconnect session functions Function Connect-EaiExchangeOnlineNonMFA { <# .SYNOPSIS Creates a PSSession to connect to Exchange Online. .DESCRIPTION This cmdlet combines the steps to successfully sign into an Exchange online tenant. .EXAMPLE Connect-EaiExchangeOnlineNonMFA To connect up to an Exchange online tenant and not use cmdlet prefix option. .EXAMPLE Connect-EaiExchangeOnlineNonMFA -prefix O365 To connect up to an Exchange online tenant and use cmdlet prefix option, with 'O365' in this example. .EXAMPLE Connect-EaiExchangeOnlineNonMFA -prefix EXO To connect up to an Exchange online tenant and use cmdlet prefix option, with 'EXO' in this example. .FUNCTIONALITY Credentials are requested for an Exchange online tenant. A PSSession is then created using URL's to the Exchange online tenant, credentials are passed, remote module imported, and a confirmation that a connection is successful is displayed to the end user. #> [cmdletbinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/connect-eaiexchangeonlinenonmfa/')] param ([parameter(HelpMessage="Optional prefix available to separate nouns for this PowerShell session from other imported PowerShell sessions in the same PowerShell run space.")] $Prefix = $Null) If ($null -eq $EXOCredential) { &$Global:EXOUserCredential } If ($Null -eq $Prefix) { $Global:EXOSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://outlook.office365.com/PowerShell/" -Credential $EXOCredential -Authentication basic -AllowRedirection Import-Module (Import-PSSession $Global:EXOSession -DisableNameChecking -AllowClobber) -Global -DisableNameChecking Connect-MsolService -Credential $Global:EXOCredential $myMBX = Get-MailBox $EXOCredential.UserName Write-Host "" Write-Host "Hello $($myMBX), you are now logged into Exchange Online." -ForegroundColor Green Write-Host "" } else { $Global:EXOSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://outlook.office365.com/PowerShell/" -Credential $EXOCredential -Authentication basic -AllowRedirection Import-Module (Import-PSSession $Global:EXOSession -DisableNameChecking -AllowClobber) -Global -DisableNameChecking -Prefix $Prefix Connect-MsolService -Credential $Global:EXOCredential $myMBX = Get-MailBox $EXOCredential.UserName Write-Verbose "`$Prefix value is: $Prefix" Write-Host "" Write-Host "Hello $($myMBX), you are now logged into Exchange Online, using a noun prefix modifier of: $Prefix." -ForegroundColor Green Write-Host "" } } Function Disconnect-EaiExchangeOnlineNonMFA { <# .SYNOPSIS Disconnects a PSSession from Exchange Online. .DESCRIPTION This cmdlet disconnects the sign-in session to an Exchange online tenant. .EXAMPLE Disconnect-EaiExchangeOnlineNonMFA This function disconnects from the current Exchange online PowerShell session. .FUNCTIONALITY Terminates the PSSession that is connected to an O365 Exchange online remote session and clears the credential variable. #> [cmdletbinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/disconnect-exchangeonlinenonemfa/')] param() Remove-PSSession $EXOSession $Credential = $null } #endregion Exchange Online connect/disconnect session functions #endregion Exchange Online sign in #region Test database status and present information of DB's: Get-EaiDAGDatabaseInformation Function Get-EaiDAGDatabaseInformation { #look into trap to stop the error of not being connected to an Exchange server <# .SYNOPSIS This cmdlet reports on the current status of databases. .DESCRIPTION Screen output of current status of databases in the Exchange environment. Lists current Databases, what the status of each database is, and how many copies are available for each Database. .EXAMPLE Get-EaiDatabaseInformation .ROLE Exchange servers. .FUNCTIONALITY Lists current Databases, what the status of each database is, and how many copies are available for each Database. #> [cmdletbinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/get-eaidagdatabaseinformation/')] Param ($Command = "Get-DatabaseAvailabilityGroup") $OldPreference = $ErrorActionPreference $ErrorActionPreference = 'stop' try { if(Get-Command $command){ if ($null -eq $TestForDAG) { "There is not a DAG in this Exchange environment." } Else { (Get-DatabaseAvailabilityGroup -Identity (Get-MailboxServer -Identity $Global:OnPremisesComputer).DatabaseAvailabilityGroup).Servers | Test-MapiConnectivity | Sort-Object Database | Format-Table -AutoSize } Get-MailboxDatabase | Sort-Object Name | Get-MailboxDatabaseCopyStatus | Format-Table -AutoSize function CopyCount { $DatabaseList = Get-MailboxDatabase | Sort-Object Name $DatabaseList | ForEach-Object { $Results = $_ | Get-MailboxDatabaseCopyStatus $Good = $Results | Where-Object { ($_.Status -eq "Mounted") -or ($_.Status -eq "Healthy") } $_ | Add-Member NoteProperty "CopiesTotal" $Results.Count $_ | Add-Member NoteProperty "CopiesFailed" ($Results.Count-$Good.Count) } $DatabaseList | Sort-Object copiesfailed -Descending | Format-Table name,copiesTotal,copiesFailed -AutoSize } CopyCount (Get-DatabaseAvailabilityGroup) | ForEach-Object {$_.Servers | ForEach-Object {Test-ReplicationHealth -Server $_}} } } Catch {Write-Host "You need to connect to an Exchange Server first, before this function will process." -ForegroundColor Red} Finally {$ErrorActionPreference=$oldPreference} } #endregion Test database status and present information of DB's: Get-DAGDatabaseInformation #region Obtain .NET version currently installed on OS. Function Get-EaiDotNETVersion { <# .SYNOPSIS Displays .NET version on a computer. .DESCRIPTION This cmdlet obtains the current version of .NET on either the local machine or a remote computer. Listed are the current version registry values and the related .NET version with the corresponding Exchange Server supported version. .EXAMPLE Get-EaiDotNETVersion Displays the current .NET version on the machine that this is run on. .NOTES For .NET values and how to obtain registry values: Start-Process "https://msdn.microsoft.com/en-us/library/hh925568(v=vs.110).aspx" .FUNCTIONALITY Displays the current release value and version of .NET. #> [cmdletbinding(HelpUri = 'https://mikeoneill.blog/Exchange_AddIn/Get-EaiDotNETVersion')] Param ([parameter( HelpMessage="Enter a computer name if needing to run this function against a remote machine.")] $Computer) If ($null -ne $Computer) { New-PSSession -ComputerName $Computer } $NetValue = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' -Name "Release" #Registry key value to test for version of .NET <#Reference page of .NET values: Start-Process "https://msdn.microsoft.com/en-us/library/hh925568(v=vs.110).aspx" #> $NetValue | Select-Object Release, @{ name=".NET Version / Supported Exchange Version(s)" expression={ switch($_.Release) { 378389 { " 4.5 / 2013 CU14" } 378675 { " 4.5.1 / 2013 CU14, CU15" } 378758 { " 4.5.1 / 2013 CU14, CU15" } 379893 { " 4.5.2 / 2013 CU14, CU15 & 2016 CU3, CU4" } 393295 { " 4.6 / None" } 393297 { " 4.6 / None" } 394254 { " 4.6.1 / 2013 CU14, CU15, CU16, & 2016 CU3, CU4" } 394271 { " 4.6.1 / 2013 CU14, CU15, CU16, & 2016 CU3, CU4" } 394802 { " 4.6.2 / 2013 CU16-CU20, & 2016 CU5-CU9" } 394806 { " 4.6.2 / 2013 CU16-CU20, & 2016 CU5-CU9" } 460798 { " 4.7 / None" } 460805 { " 4.7 / None" } 461308 { " 4.7.1 / 2013 CU19-CU22, & 2016 CU8-CU12" } 461310 { " 4.7.1 / 2013 CU19-CU23, & 2016 CU6-CU12" } 461808 { " 4.7.2 / 2013 CU21-CU23, & 2016 CU11-CU14, & 2019TRM-CU3" } 461814 { " 4.7.2 / 2013 CU21-CU23, & 2016 CU11-CU14, & 2019TRM-CU3" } {$_ -ge 528040} { " 4.8 / 2013 CU23, & 2016 CU14-CU20, & 2019 CU2 and later" } #528040 { " 4.8 / 2013 CU23, & 2016 CU14-CU20, & 2019 CU2 and later" } #528049 { " 4.8 / 2013 CU23, & 2016 CU14-CU20, & 2019 CU2 and later" } #528209 { " 4.8 / 2013 CU23, & 2016 CU14-CU20, & 2019 CU2 and later" } #528372 { " 4.8 / 2013 CU23, & 2016 CU14-CU20, & 2019 CU2 and later" } default {"Unknown version of .NET and unknown support of Exchange Server"} } } } } #endregion Obtain .NET version currently installed on OS. #region Start DAG Maintenance Mode Function Start-EaiDAGMaintenanceMode { <# .SYNOPSIS Function to put an Exchange 2010/2013/2016/2019 Server into Maintenance Mode. Credits: -------- Exchange Server 2013 Maintenance Mode Script (Start): https://gallery.technet.microsoft.com/exchange/Exchange-Server-2013-ff6c942f Checking for admin credentials: http://blogs.technet.com/b/heyscriptingguy/archive/2011/05/11/check-for-admin-credentials-in-a-powershell-script.aspx .DESCRIPTION This function is created to automatically put an Exchange 2010/2013/2016/2019 Server into Maintenance Mode. It will detect if the server is a Mailbox Server and then take appropriate additional actions, if any. .EXAMPLE Start-EaiExchangeServerMaintenanceMode -Server Server1 Running the following command will place a server called "Server1" into Maintenance Mode. .EXAMPLE Start-EaiExchangeServerMaintenanceMode -Server Server1 -TargetServerFQDN Server2.domain.com Running the following command will place a server called "Server1" into Maintenance Mode and move any messages in transit from that server to "Server2". Please note that the TargetServer value has to be a FQDN! #> [CmdletBinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/start-eaidagmaintenancemode/')] Param ( # determine what server to put in maintenance mode [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0)] [string]$Server, [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true, Position=1)] [string]$TargetServerFQDN = $Server ) function evaluatequeues(){ $MessageCount = Get-Queue -Server $Server | Where-Object {$_.Identity -notlike "*\Poison" -and $_.Identity -notlike"*\Shadow\*"} | Select-Object MessageCount $count = 0 Foreach($message in $MessageCount){ $count += $message.messageCount } if($count -ne 0){ Write-Output "INFO: Sleeping for 30 seconds before checking the transport queues again..." -ForegroundColor Yellow Start-Sleep -s 30 evaluatequeues } else{ Write-Host "INFO: Transport queues are empty." -ForegroundColor Yellow Write-Host "INFO: Putting the entire server into maintenance mode..." -ForegroundColor Yellow if(Set-ServerComponentState $Server -Component ServerWideOffline -State Inactive -Requester Maintenance){ Write-Host "INFO: Done! The components of $Server have successfully been placed into an inactive state!" } Write-Host "INFO: Restarting MSExchangeTransport service on server $Server..." -ForegroundColor Yellow #Restarting transport services based on info from http://blogs.technet.com/b/exchange/archive/2013/09/26/server-component-states-in-exchange-2013.aspx #Restarting the services will cause the transport services to immediately pick up the changed state rather than having to wait for a MA responder to take action Invoke-Command -ComputerName $Server {Restart-Service MSExchangeTransport | Out-Null} #restart Front End Transport Services if server is also CAS if($discoveredServer.IsFrontendTransportServer -eq $true){ Write-Host "INFO: Restarting the MSExchangeFrontEndTransport Service on server $Server..." -ForegroundColor Yellow Invoke-Command -ComputerName $Server {Restart-Service MSExchangeFrontEndTransport} | Out-Null } Write-Host "`nINFO: Done! Server $Server is put succesfully into maintenance mode!`n" -ForegroundColor Green } } $discoveredServer = Get-ExchangeServer -Identity $Server | Select-Object IsHubTransportServer,IsFrontendTransportServer,AdminDisplayVersion #Check for Administrative credentials If (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){ Write-Warning "You do not have Administrator rights to run this script!`nPlease re-run this script as an Administrator!" Break } if($discoveredServer.IsHubTransportServer -eq $True){ if(-NOT ($TargetServerFQDN)){ Write-Warning "TargetServerFQDN is required." $TargetServerFQDN = Read-Host -Prompt "Please enter the TargetServerFQDN: " } #Get the FQDN of the Target Server through DNS, even if the input is just a host name try{ $TargetServer = ([System.Net.Dns]::GetHostByName($TargetServerFQDN)).Hostname } catch{ Write-Warning "Could not resolve ServerFQDN: $TargetServerFQDN";break } if((Get-ExchangeServer -Identity $TargetServer | Select-Object IsHubTransportServer).IsHubTransportServer -ne $True){ Write-Warning "The target server is not a valid Mailbox server." Write-Warning "Aborting script..." Break } #Redirecting messages to target system Write-Host "INFO: Suspending Transport Service. Draining remaining messages..." -ForegroundColor Yellow Set-ServerComponentState $Server -Component HubTransport -State Draining -Requester Maintenance Redirect-Message -Server $Server -Target $TargetServer -Confirm:$false #suspending cluster node (if the server is part of a DAG) $mailboxserver = Get-MailboxServer -Identity $Server | Select-Object DatabaseAvailabilityGroup if($null -ne $mailboxserver.DatabaseAvailabilityGroup){ Write-Host "INFO: Server $Server is a member of a Database Availability Group. Suspending the node now.`n" -ForegroundColor Yellow Write-Host "INFO: Node information:" -ForegroundColor Yellow Write-Host "-----------------------" -ForegroundColor Yellow Write-Host "" Write-Host "" Invoke-Command -ComputerName $Server -ArgumentList $Server {Suspend-ClusterNode $args[0]} Set-MailboxServer $Server -DatabaseCopyActivationDisabledAndMoveNow $true Set-MailboxServer $Server -DatabaseCopyAutoActivationPolicy Blocked } #Evaluate the Transport Queues and put into maintenance mode once all queues are empty evaluatequeues } else{ Write-Host "INFO: Server $Server is a Client Access Server-only server." -ForegroundColor Yellow Write-Host "INFO: Putting the server components into inactive state" -ForegroundColor Yellow Set-ServerComponentState $Server -Component ServerWideOffline -State Inactive -Requester Maintenance Write-Host "INFO: Restarting transport services..." -ForegroundColor Yellow if(Invoke-Command -ComputerName $Server {Restart-Service MSExchangeFrontEndTransport | Out-Null}){ Write-Host "INFO: Successfully restarted MSExchangeFrontEndTransport service" -ForegroundColor Yellow } Write-Host "`nINFO: Done! Server $Server is put succesfully into maintenance mode!`n" -ForegroundColor Green } } #endregion Start DAG Maintenance Mode #region Stop DAG Maintenance Mode Function Stop-EaiDAGMaintenaceMode { <# .SYNOPSIS Function to take an Exchange 2010/2013/2016/2019 Server out of Maintenance Mode. Credits: -------- Exchange Server 2013 Maintenance Mode Script (Stop): https://gallery.technet.microsoft.com/Exchange-Server-2013-77a71eb2 Checking for admin credentials: http://blogs.technet.com/b/heyscriptingguy/archive/2011/05/11/check-for-admin-credentials-in-a-powershell-script.aspx .DESCRIPTION This function is created to take an Exchange 2010/2013/2016/2019 Server out of Maintenance Mode. It will detect if the server is a Mailbox Server and then take appropriate additional actions, if any. .EXAMPLE Stop-EaiExchangeServerMaintenanceMode -Server Server1 Running the following command will take a server called "Server1" out of Maintenance Mode: #> [CmdletBinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/stop-eaidagmaintenancemode/')] Param ( # determine what server to put in maintenance mode [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0)] [string]$Server ) $discoveredServer = Get-ExchangeServer -Identity $Server | Select-Object IsHubTransportServer,IsFrontendTransportServer,AdminDisplayVersion #Check for Administrative credentials If (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){ Write-Warning "You do not have Administrator rights to run this script!`nPlease re-run this script as an Administrator!" Break } Write-Host "INFO: Reactivating all server components..." -ForegroundColor Yellow Set-ServerComponentState $server -Component ServerWideOffline -State Active -Requester Maintenance Write-Host "INFO: Server component states changed back into active state using requester 'Maintenance'" -ForegroundColor Yellow if($discoveredServer.IsHubTransportServer -eq $true){ $mailboxserver = Get-MailboxServer -Identity $Server | Select-Object DatabaseAvailabilityGroup if($null -ne $mailboxserver.DatabaseAvailabilityGroup){ Write-Host "INFO: Server $server is a member of a Database Availability Group. Resuming the node now.`n" -ForegroundColor Yellow Write-Host "INFO: Node information:" -ForegroundColor Green Write-Host "-----------------------" -ForegroundColor Green Write-Host "" Invoke-Command -ComputerName $Server -ArgumentList $Server {Resume-ClusterNode $args[0]} Set-MailboxServer $Server -DatabaseCopyActivationDisabledAndMoveNow $false Set-MailboxServer $Server -DatabaseCopyAutoActivationPolicy Unrestricted } Write-Host "INFO: Resuming Transport Service..." -ForegroundColor Yellow Set-ServerComponentState -Identity $Server -Component HubTransport -State Active -Requester Maintenance Write-Host "INFO: Restarting the MSExchangeTransport Service on server $Server..." -ForegroundColor Yellow Invoke-Command -ComputerName $Server {Restart-Service MSExchangeTransport} | Out-Null } #restart Front End Transport Services if($discoveredServer.IsFrontendTransportServer -eq $true){ Write-Host "INFO: Restarting the MSExchangeFrontEndTransport Service on server $Server..." -ForegroundColor Yellow Invoke-Command -ComputerName $Server {Restart-Service MSExchangeFrontEndTransport} | Out-Null } Write-Host "" Write-Host "INFO: Done! Server $server successfully taken out of Maintenance Mode." -ForegroundColor Green Write-Host "" $ComponentStates = (Get-ServerComponentstate $Server).LocalStates | Where-Object {$_.State -eq "InActive"} if($ComponentStates){ Write-Warning "There are still some components inactive on server $Server." Write-Warning "Some features might not work until all components are back in an Active state." Write-Warning "Check the information below to see what components are still in an inactive state and which requester put them in that state." $ComponentStates Clear-Variable ComponentStates } } #endregion Stop DAG Maintenance Mode #region Update a Specific Distribution List Function Update-EaiSpecificDLCSVImport { <# .SYNOPSIS Function to update a specific DL. .DESCRIPTION Simple function to import a CSV file with PrimarySMTPAddress as a column into a specific Distribution Group. The cmdlet of Update-DistributionGroupMember replaces all addresses that are currently in the DL with the new list of members. https://technet.microsoft.com/en-us/library/dd335049(v=exchg.160).aspx .EXAMPLE Update-EaiSpecificDLCSVImport -ImportPath c:\temp\file.csv -GroupToUpdate MyGroup This updates the 'MyGroup' DL with the content listed in c:\temp\file.csv file. .EXAMPLE Update-EaiSpecificDLCSVImport -ImportPath c:\users.csv -GroupToUpdate SecurityGroup This updates the 'SecurityGroup' DL with the content listed in c:\users.csv file. .INPUTS Import Path of CSV file that contains PrimarySMTPAddress as a column. .OUTPUTS Displays current list of users in requested group. #> [cmdletbinding(HelpUri="https://mikeoneill.blog/Exchange_AddIn/update-eaispecificdlcsvimport/")] param ( [Parameter(Mandatory=$true, HelpMessage="Enter the import path of the file location, including the file name.")] $ImportPath, [Parameter(Mandatory=$true, HelpMessage="You must enter the name of the distribution group to update.")] $GroupToUpdate) # Import the members from the CSV $memberobjs = Import-Csv $ImportPath # Convert the member objects from the CSV into a property independent array # You can change PrimarySMTPAddress to be any property in the CSV that the Update-DistributionGroupmember cmdlet will take $memberobjs | ForEach-Object {[array]$newmembers = $newmembers + $_.PrimarySMTPAddress} # Update the DG membership Update-DistributionGroupmember -Identity $GroupToUpdate -members $newmembers -Confirm:$false # Output the new DG Membership Get-DistributionGroupMember -Identity $GroupToUpdate } Function Update-EaiSpecificDLTXTImport { <# .SYNOPSIS Function to update a specific DL. .DESCRIPTION Simple function to import a TXT file with PrimarySMTPAddress listings, no column header is needed. The cmdlet of Update-DistributionGroupMember replaces all addresses that are currently in the DL with the new list of members. https://technet.microsoft.com/en-us/library/dd335049(v=exchg.160).aspx .EXAMPLE Update-EaiSpecificDLTXTImport -ImportPath c:\temp\file.txt -GroupToUpdate MyGroup This updates the 'MyGroup' DL with the content listed in c:\temp\file.txt file. .EXAMPLE Update-EaiSpecificDLTXTImport -ImportPath c:\users.txt -GroupToUpdate SecurityGroup This updates the 'SecurityGroup' DL with the content listed in c:\users.txt file. .INPUTS Import Path of TXT file that contains PrimarySMTPAddress as a list. .OUTPUTS Displays current list of users in requested group. #> [cmdletbinding(HelpUri="https://mikeoneill.blog/Exchange_AddIn/update-eaispecificdltxtimport/")] param ( [Parameter(Mandatory=$true, HelpMessage="Enter the import path of the file location, including the file name.")] $ImportPath, [Parameter(Mandatory=$true, HelpMessage="You must enter the name of the distribution group to update.")] $GroupToUpdate ) # Create array of users via the Get-Content import cmdlet $newmembers = Get-Content $ImportPath # Update the DG membership Update-DistributionGroupmember -Identity $GroupToUpdate -Members $newmembers -Confirm:$false # Output the new DG Membership Get-DistributionGroupMember -Identity $GroupToUpdate } #endregion Update a Specific Distribution List #region restart IIS on all Exchange Servers in organization Function Restart-EaiExchangeIIS { <# .SYNOPSIS Restarts the IIS process on all Exchange servers in an Exchange organization. .DESCRIPTION This function creates an array of all Exchange servers in an organization. Then it sends out an IIS restart command via PowerShell to all servers listed in the array. .EXAMPLE Restart-EaiExchangeIIS Will request all Exchange servers with Get-ExchangeServer and restart IIS on the array built. .ROLE Must already be logged onto an Exchange server PowerShell session for Get-ExchangeServer cmdlet to process. #> [cmdletbinding(HelpUri = 'https://mikeoneill.blog/Exchange_AddIn/Restart-EaiExchangeIIS')] param() $ExchangeServers = Get-ExchangeServer #Must be logged in remotely to Exchange server or running this via Exchange Management Shell Write-Verbose "`$ExchangeServers array contains: $ExchangeServers" ForEach ($ExchangeServer in $ExchangeServers) { Write-Host "Restarting the w3svc service on $($ExchangeServer.name)" -ForegroundColor Yellow Invoke-Command -ComputerName $ExchangeServer.name {Restart-Service w3svc -force} $Service = Get-Service -Name w3svc -ComputerName $ExchangeServer.name Write-Host "$($ExchangeServer.name) web service is $($Service.status)" -ForegroundColor Green Write-Host "" } } #endregion restart IIS on all Exchange Servers in organization #region restart IIS on specific servers Function Restart-EaiIISOnServers { <# .SYNOPSIS Restart IIS on servers. .DESCRIPTION Primarily designed to be used on Exchange servers, if IIS needs a restart, this function can be used on any servers running IIS. Load the parameter with a sinlge server or multiple servers wanting to process, then the invoke-command creates a remote connection to restart the IIS service. .EXAMPLE Restart-EaiIISOnServers -Computer PC1,PC2,PC3,PC4 This will restart IIS on the following computers: PC1, PC2, PC3, PC4 .EXAMPLE Restart-EaiIISOnServers -Computer DC1 This will restart IIS on a single computer: DC1 .INPUTS Enter a single name or multiple names separated by commas to restart IIS on servers. #> [cmdletbinding(HelpUri = 'https://mikeoneill.blog/exchange_addin/restart-eaiiisonservers/')] param ([Parameter(Mandatory = $true, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$true, HelpMessage="Enter a single server or multiple server names, separated by commas, to restart IIS on the target computer(s).")] $Computer) #Get-content -path c:\temp\listOfServersToBounce.txt = $ExchangeServers line to fix. Write-Verbose "`$Computer array contains: $Computer" ForEach ($Comp in $Computer) { Write-Host "Restarting the w3svc service on $Comp" -ForegroundColor Yellow Invoke-Command -ComputerName $Comp {Restart-Service w3svc -force} $Service = Get-Service -Name w3svc -ComputerName $Comp Write-Host "$Comp web service is $($Service.status)" -ForegroundColor Green Write-Host "" } $Computer = $null } #endregion restart IIS on specific servers #region restart IIS AutoD app pool, less abrasive than restarting IIS on all Exchange servers Function Restart-EaiAutoDAppPool { <# .SYNOPSIS Restarts IIS AutoDiscover app pool on all Exchange servers in Organization. .DESCRIPTION Obtains all Exchange servers in the organization and restarts the IIS AutoDiscover application pool on all servers. .EXAMPLE Restart-EaiAutoDAppPool Will request all Exchange servers with Get-ExchangeServer and restart AutoDiscover app pool on the array of servers built. .NOTES This function allows repeatable use of restarting the IIS app pool for AutoDiscover on Exchange servers. This function will not restart the entire IIS service, just the specific AutoD app pool. Must be logged into an Exchange server remotely via PowerShell or Exchange Management Shell. #> [cmdletbinding(helpuri='https://mikeoneill.blog/Exchange_AddIn/Restart-EaiAutoDAppPool/', SupportsShouldProcess=$true,ConfirmImpact='High')] Param() $ExchangeServers = Get-ExchangeServer #Must be logged in remotely to Exchange server or running this via Exchange Management Shell Write-Verbose "`$ExchangeServers array contains: $ExchangeServers" $ExchangeServers | ForEach-Object { if ($pscmdlet.ShouldProcess("$_", "Restarting AutoDiscover application pool")) { Write-Host "Recycling AutoDiscover application pool on $_" -ForegroundColor Yellow Invoke-Command -ComputerName $_.name -ScriptBlock { Get-ChildItem IIS:\AppPools | Where-Object { $_.name -like "*autodisc*" } | Restart-WebAppPool } } } $ExchangeServers = $null } #endregion restart IIS AutoD app pool, less abrasive than restarting IIS on all Exchange servers #region Get: SPF, DMARC, DKIM from O365 tenant Function Get-EaiEXOEmailSecuritySettings { <# .SYNOPSIS Present the current Email Security Settings of an o365 tenant. .DESCRIPTION Shows the: SPF, DKIM, and DMAC settings of a tenant. This function confirms the status of these settings in order to review if the different e-mail security settings have been enabled and/or are configured. .EXAMPLE Get-EaiEXOEmailSecuritySettings Gets the currently logged in tenant settings. .EXAMPLE Get-EaiEXOEmailSecuritySettings -AcceptedDomains contoso.com Gets the contoso.com in tenant settings. .NOTES Creator: Matt Fields - PFE Updated: Mike O'Neill - PFE Version 1.0 - created code and functioned up for repeatable usage. #> [cmdletbinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/get-eaiexoemailsecuritysettings/')] param($AcceptedDomains = $(Get-AcceptedDomain)) Write-Verbose "Accepted domains obtained: $($AcceptedDomains)" ForEach ($Domain in $AcceptedDomains) { #SPF Check $SPF = Resolve-DnsName -Type TXT -Name $Domain -ErrorAction SilentlyContinue | Where-Object {$_.Strings -like "v=spf1*"} Write-Verbose "Domain working on: $($domain)" Write-Verbose "`$spf array contains: $spf" If (!($SPF)) { Write-Host -ForegroundColor White -BackgroundColor Red "SPF has not been configured for $($Domain.DomainName)" Write-Host } Else { $SPF | Format-Table -AutoSize } #DKIM Check $DKIM = Get-DKIMSigningConfig $Domain.DomainName Write-Verbose "`$DKIM array contains: $DKIM" IF ($DKIM.Enabled -eq 'True') { $DKIM = Resolve-DnsName -ErrorAction SilentlyContinue -Type CNAME -Name "selector1._domainkey.$($Domain.DomainName)" IF (!($DKIM)) { Write-Host -ForegroundColor White -BackgroundColor Red "DKIM is enabled, but DNS entries have not been created for $($Domain.DomainName)" Write-Host } } Else { Write-Host -ForegroundColor White -BackgroundColor Red "DKIM is not enabled for $($Domain.DomainName)" Write-Host } #DMARC Check $DMARC = Resolve-DnsName -ErrorAction SilentlyContinue -Type TXT -Name "_dmarc.$($Domain.DomainName)" | Where-Object {$_.Strings -like "v=DMARC1*"} Write-Verbose "`$DMARC array contains: $DMARC" If (!($DMARC)) { Write-Host -ForegroundColor White -BackgroundColor Red "DMARC has not been configured for $($Domain.DomainName)" Write-Host } Else { $DMARC | Format-Table -AutoSize } } } #endregion SPF, DMARC, DKIM from O365 tenant #region Get-EaiDistributionGroupMembersRecursive function Get-EaiDistributionGroupMembersRecursive { <# .SYNOPSIS Get all members of a distribution group, including nested groups. .DESCRIPTION This function will get all members of a distribution group, including nested groups that could be within the distribution group itself. This function also will look at any looping groups and skip them to prevent an infinite loop. This function will not get members of a security group, only e-mail enabled distribution groups. This function will not get members of a dynamic distribution group. The output is a custom object with the following properties: TotalUserCount, TotalGroupCount, TotalCount TotalUserCount is the number of recipients in the group all of the groups. TotalGroupCount is the number of groups in the group and all of the groups that would get messages sent to them. TotalCount is the total number of recipients and groups in the group and all of the groups that would get messages sent to them. .EXAMPLE Get-EaiDistributionGroupMembersRecursive -GroupName 'IT department' This will get all members of the 'IT department' distribution group, including nested groups. .EXAMPLE Get-EaiDistributionGroupMembersRecursive -GroupName 'Sales Team' -Verbose This will get all members of the 'Sales Team' distribution group, including nested groups, including any Verbose output. .INPUTS Group name for distribution group to be processed. .OUTPUTS Screen output of the total number of users and groups in the distribution group and all nested groups. .NOTES Created by Mike O'Neill Original code from the following blog post: https://blogs.technet.microsoft.com/heyscriptingguy/2015/03/31/use-powershell-to-find-members-of-distribution-groups/ Edited with assistance from: Kory Thatcher .FUNCTIONALITY This can be performed in either Exchange online or Exchange on premises servers. The shell needs to be connected to the Exchange environment first. #> [CmdletBinding()] param ( [string] $GroupName, [Parameter(DontShow = $true)] [System.Collections.ArrayList]$groupsToSkip = (new-object -TypeName System.Collections.ArrayList) ) if ($GroupName -in $groupsToSkip) { Write-Verbose "Skipping group $GroupName as it has already been processed" return 0 } $groupsToSkip.add($GroupName) | Out-Null #Get-DistributionGroupMember -Identity 'it department' $groupMembers = Get-DistributionGroupMember -Identity $GroupName -ResultSize Unlimited $AllCounts = [PSCustomObject]@{ TotalUserCount = 0 TotalGroupCount = 0 TotalCount = $groupMembers.Count } Write-Verbose "Total members in group $GroupName including without nested groups: $AllCounts" foreach ($member in $groupMembers) { #create array of found groups if ($member.RecipientType -eq "MailUniversalDistributionGroup") { Write-Verbose "Getting group of nested group $($member.DisplayName)" $AllCounts.TotalGroupCount++ $ReturnCount = Get-EaiDistributionGroupMembersRecursive -GroupName $member.DisplayName -groupsToSkip ([ref]$groupsToSkip) $AllCounts.TotalUserCount+=$ReturnCount.TotalUserCount $AllCounts.TotalGroupCount+=$ReturnCount.TotalGroupCount $AllCounts.totalCount+=$ReturnCount.totalCount } else{ $AllCounts.TotalUserCount++ } } Write-Verbose "Total members in group $GroupName including nested groups: $AllCounts" return $AllCounts } #endregion Get-EaiDistributionGroupMembersRecursive #region Get domain specific OOF settings Function Get-EaiMailboxAutoReplyConfigurationDomain { <# .SYNOPSIS Lists OOF settings for users within the domain of specific SMTP addresses in a domain. .DESCRIPTION Lists OOF settings if they are enabled and what values are set for users. This function only displays enabled OOF's and not disabled ones. .EXAMPLE Get-EaiMailboxAutoReployConfigurationDomain -Domain Contoso.com This lists end users with an @contoso.com SMTP address if they have OOF settings enabled. .EXAMPLE Get-EaiMailboxAutoReployConfigurationDomain -Domain contoso.onmicrosoft.com This lists end users with an @contoso.onmicrosoft.com SMTP address if they have OOF settings enabled. .INPUTS Need to define the domain parameter. .OUTPUTS Output to the screen of values that have OOF set. #> [cmdletbinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/get-eaimailboxautoreplyconfigurationdomain/')] param([Parameter(Mandatory=$true, HelpMessage="Enter the domain value to be selected for scanning OOF values.")] $Domain) Write-Verbose "`$Domain array contains: $Domain" $Mailboxes=Get-Mailbox -ResultSize Unlimited | Where-Object {$_.PrimarySmtpAddress -like "*$Domain"} Write-Verbose "`$Mailboxes array contains: $Mailboxes" ForEach ($Mailbox in $Mailboxes) { $Mailbox | Get-MailboxAutoReplyConfiguration | Where-Object { $_.AutoReplyState -ne "Disabled" } | Select-Object Identity,StartTime,EndTime,AutoReplyState } <#Demo to set for testing, change UserName to a valid value in the organization: Scheudle OOF for testing of user. Set-MailboxAutoReplyConfiguration -Identity UserName -AutoReplyState Scheduled -StartTime "$(Get-Date).adddays(1)" -EndTime "$(Get-Date).addmonths(1)" -InternalMessage "Internal auto-reply message" Disable OOF testing for user. Set-MailboxAutoReplyConfiguration -Identity UserName -AutoReplyState disabled #> } #endregion Get domain specific OOF settings #region Get membership count of distribution and security groups Function Get-EaiGroupMemberCount { <# .SYNOPSIS Get member count of groups. .DESCRIPTION Gets all distribution groups and security enabled groups and presents name and member count of each group. This does NOT include nested groups, only the count of the groups listed which is all groups in the organization. Outputs the content into a csv file. .EXAMPLE Get-EaiGroupMemberCount This will obtain distribution group list in CSV export. Default path is: $env:temp\DistributionGroups.csv .EXAMPLE Get-EaiGroupMemberCount -path c:\temp -FileName DGs.csv This will obtain distribution group list in CSV exported file to c:\temp titled DGs.csv .OUTPUTS CSV file titled: SecurityGroups.csv in path parameter, default "$env:temp" which is: 'c:\Users\<UserName>\AppData\Local\Temp' #> [cmdletbinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/Get-EaiGroupMemberCount/')] param ($Path = $env:TEMP, $FileName = "DistributionGroups.csv") $groups = @(Get-DistributionGroup -ResultSize unlimited) #Gets all distribution groups or e-mail enabled security groups. Write-Verbose "`$groups array contains: $groups" $report = @() #Creates empty array for report. ForEach ($group in $groups) { $Count = @(Get-DistributionGroupMember -Identity $group.DistinguishedName).Count $ReportObject = New-Object PSObject $ReportObject | Add-Member NoteProperty -Name "GroupName" -Value $group.name $ReportObject | Add-Member NoteProperty -Name "Count" -value $Count $Report += $ReportObject } $Report | Export-Csv -Path $Path\$FileName -NoTypeInformation Write-Verbose "`$Groups array contains: $groups" Write-Verbose "`$Path array contains: $Path" Write-Verbose "`$FileName array contains: $FileName" If (Test-Path -Path $Path\$FileName) { Write-Host "File $Path\$FileName exists." -ForegroundColor Green } Else { Write-Host "File $myDir\$FileName was not created." -ForegroundColor Red } } #endregion Get membership count of distribution and security groups #region Get Outlook bit value Function Get-EaiOutlookBitValue { <# .SYNOPSIS Get-EaiOutlookBitValue .DESCRIPTION Obtains Outlook bit version, 32 or 64, for a local Windows computer. .EXAMPLE Get-EaiOutlookBitValue Displays current version of Outlook if it is x86 (32bit) or x64 (64bit) .OUTPUTS Outputs to screen .NOTES Only currently works against a local machine. No remote parameter built in for function. #> [cmdletbinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/Get-EaiOutlookBitValue')] Param() "WOW6432Node\","" | ForEach-Object { Get-ChildItem -Path "HKLM:\SOFTWARE\$($_)Microsoft\Office" } | Where-Object { (Get-ItemProperty -ea Ignore -Path "$($_.PsPath)\Outlook").Bitness } | Format-Table -AutoSize -Prop @{N="RegKey";E={$_.Name}},@{N="Bitness";E={(Get-ItemProperty -Path "$($_.PsPath)\Outlook").Bitness}} "WOW6432Node\","" | ForEach-Object { Get-ChildItem -Path "HKLM:\SOFTWARE\$($_)Microsoft\Office" } | Where-Object { $_.PSChildName -match "\d" } | Where-Object { Test-Path "$($_.PsPath)\Outlook" } | Where-Object { (Get-ItemProperty -Path "$($_.PsPath)\Outlook").Bitness } | Format-Table -AutoSize -Prop @{N="RegKey";E={$_.Name}},@{N="Bitness";E={(Get-ItemProperty -Path "$($_.PsPath)\Outlook").Bitness}} [string[]]$outlookRegKeys = 'HKLM:\SOFTWARE\Microsoft\Office\14.0\Outlook', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Office\14.0\Outlook', 'HKLM:\SOFTWARE\Microsoft\Office\15.0\Outlook', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Office\15.0\Outlook', 'HKLM:\SOFTWARE\Microsoft\Office\16.0\Outlook', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Office\16.0\Outlook' foreach ($outlookRegKey in $outlookRegKeys) { if (Test-Path $outlookRegKey) { $outlook = Get-ItemProperty -Path $outlookRegKey -Name 'Bitness' if ($outlook.Bitness -match '86') { Write-Host 'Outlook version is: x86 (32bit)' -ForegroundColor Green } else { Write-Host 'Outlook version is: x64 (64bit)' -ForegroundColor Green } } } } #endregion Get Outlook bit value #region Get-EaiMailboxLocations Function Get-EaiMailboxLocations { <# .SYNOPSIS Displays locations of mailboxes in Exchange Online. .DESCRIPTION Determines the number of datacenters and locations where Exchange Online mailboxes are distributed. .EXAMPLE Get-EaiMailboxLocations No parameters are needed. .OUTPUTS Displays content to screen. .NOTES Code used from Joe Palarchio from the Get-MailboxLocations.ps1 file. https://blogs.perficient.com/2016/03/15/office-365-script-to-determine-exchange-online-mailbox-location/ Limitations: Table of datacenters is static and may need to be expanded as Microsoft brings additional datacenters online. This function runs 'Get-Mailbox' which can take a really long time in EXO if the tenant has lots of mailboxes. Added display of geo-location of mailboxes, if they exist. Geo-location of mailbox docs page: Start-Process https://docs.microsoft.com/en-us/microsoft-365/enterprise/administering-exchange-online-multi-geo?view=o365-worldwide This is not the 'Get-MailboxLocation' cmdlet: https://docs.microsoft.com/en-us/powershell/module/exchange/mailboxes/get-mailboxlocation?view=exchange-ps .COMPONENT Remote PowerShell connection to Exchange Online is required. #> [cmdletbinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/Get-EaiMailboxLocations')] param() try { $ExSession = @() $ExSession = Get-PSSession | Where-Object {(($_.name -match 'Exchangeonline*') -and ($_.computername -eq 'outlook.office365.com') -and ($_.configurationName -eq 'Microsoft.Exchange'))} if (($ExSession.count -eq 0) -xor ((Get-ConnectionInformation).TokenStatus -eq 'Active')){throw "No connected sessions"} } catch { Write-Host 'You are not connected to an EXOv2 or EXOv3 PowerShell session.' break } $Datacenter = @{} $Datacenter["CP"]=@("LAM","Brazil") $Datacenter["GR"]=@("LAM","Brazil") $Datacenter["HK"]=@("APC","Hong Kong") $Datacenter["SI"]=@("APC","Singapore") $Datacenter["SG"]=@("APC","Singapore") $Datacenter["KA"]=@("JPN","Japan") $Datacenter["OS"]=@("JPN","Japan") $Datacenter["TY"]=@("JPN","Japan") $Datacenter["AM"]=@("EUR","Amsterdam, Netherlands") $Datacenter["DB"]=@("EUR","Dublin, Ireland") $Datacenter["HE"]=@("EUR","Finland") $Datacenter["VI"]=@("EUR","Austria") $Datacenter["BL"]=@("NAM","Virginia, USA") $Datacenter["SN"]=@("NAM","San Antonio, Texas, USA") $Datacenter["BN"]=@("NAM","Virginia, USA") $Datacenter["DM"]=@("NAM","Des Moines, Iowa, USA") $Datacenter["BY"]=@("NAM","San Francisco, California, USA") $Datacenter["CY"]=@("NAM","Cheyenne, Wyoming, USA") $Datacenter["CO"]=@("NAM","Quincy, Washington, USA") $Datacenter["MW"]=@("NAM","Quincy, Washington, USA") $Datacenter["CH"]=@("NAM","Chicago, Illinois, USA") $Datacenter["ME"]=@("APC","Melbourne, Victoria, Australia") $Datacenter["SY"]=@("APC","Sydney, New South Wales, Australia") $Datacenter["KL"]=@("APC","Kuala Lumpur, Malaysia") $Datacenter["PS"]=@("APC","Busan, South Korea") $Datacenter["YQ"]=@("CAN","Quebec City, Canada") $Datacenter["YT"]=@("CAN","Toronto, Canada") Write-Host Write-Host "Gathering mailbox information..." -ForegroundColor Yellow $Mailboxes = Get-Mailbox -ResultSize Unlimited | Where-Object {$_.RecipientTypeDetails -ne "DiscoveryMailbox"} $ServerCount = ($Mailboxes | Group-Object {$_.ServerName}).count $Mailboxes = $Mailboxes | Group-Object {$_.ServerName.SubString(0,2)} | Select-Object @{Name="Datacenter";Expression={$_.Name}}, Count $Locations=@() # Not pretty error handling but allows counts to add properly when a datacenter location could not be identified from the table $E = $ErrorActionPreference $ErrorActionPreference = "SilentlyContinue" ForEach ($Mailbox in $Mailboxes) { $Object = New-Object -TypeName PSObject $Object | Add-Member -Name 'Datacenter' -MemberType NoteProperty -Value $Mailbox.Datacenter $Object | Add-Member -Name 'Region' -MemberType NoteProperty -Value $Mailbox.MailboxRegion $Object | Add-Member -Name 'Location' -MemberType NoteProperty -Value $Datacenter[$Mailbox.Datacenter][1] $Object | Add-Member -Name 'Count' -MemberType NoteProperty -Value $Mailbox.Count $Object | Add-Member -Name 'MailboxregionLastupdateTime' -MemberType NoteProperty -Value $Mailbox.MailboxregionLastupdateTime $Object | Add-Member -Name 'MailboxRegionSuffix' -MemberType NoteProperty -Value $Mailbox.MailboxRegionSuffix $Locations += $Object } $ErrorActionPreference = $E $TotalMailboxes = ($Locations | Measure-Object Count -Sum).sum $LocationsConsolidated = $Locations | Group-Object Location | ForEach-Object { New-Object PSObject -Property @{ Location = $_.Name Mailboxes = ($_.Group | Measure-Object Count -Sum).Sum } } | Sort-Object Count -Descending Write-Host Write-Host -NoNewline "Your " Write-Host -NoNewline -ForegroundColor Yellow $TotalMailboxes Write-Host -NoNewline " mailboxes are spread across " Write-Host -NoNewline -ForegroundColor Yellow $ServerCount Write-Host -NoNewline " servers in " Write-Host -NoNewline -ForegroundColor Yellow $Locations.Count Write-Host -NoNewline " datacenters in " Write-Host -NoNewline -ForegroundColor Yellow $LocationsConsolidated.Count Write-Host " geographical locations." Write-Host Write-Host "The distribution of mailboxes is shown below:" $LocationsConsolidated | Select-Object Location, Mailboxes, Mailboxregion, MailboxRegionLastUpdateTime, MailboRegionSuffix } #endregion Get-EaiMailboxLocations #region Get-EaiExchangeServerDBWhiteSpace Function Get-EaiExchangeServerDBWhiteSpace { <# .SYNOPSIS Displays white space on all Exchange databases in organization. .DESCRIPTION This function obtains current white space in all databases within an Exchange Server organization. This function could take a while to run the Get-MailboxDatabase -status for all Databases in organization. No parameters needed, it will just run against all Databases. .EXAMPLE Get-EaiExchangeDBWhiteSpace This will display all of the databases within the Exchange Server organization and their realative current white space. .OUTPUTS Displays content to screen. .ROLE Current PowerShell session needs to be used, either EMS on an Exchange server, or a remote PowerShell session connected to an Exchange server. #> [cmdletbinding(HelpUri='https://mikeoneill.blog/Exchange_AddIn/Get-EaiExchangeServerDBWhiteSpace')] param () Get-MailboxDatabase -Status | ForEach-Object { $DBName = $_.Name $whiteSpace = $_.AvailableNewMailboxSpace.ToMb() Write-Host "The $DBName database has $whiteSpace MB of total white space." } } #endregion Get-EaiExchangeServerDBWhiteSpace #region Set ADAutoD site Function Export-CSVAllADSites { <# .SYNOPSIS Exports all AD sites within a domain. .DESCRIPTION Creates a CSV file to list out all of the sites and some of their properties within columns. This information can then be used for documentation or setting up an Exchange server site update for autodiscover site values. .EXAMPLE Function Export-CSVAllADSites This will export AD sites in the current forest to the default path of: $env:temp\ADSiteInfo.csv .EXAMPLE Function Export-CSVAllADSites -ReportFilePath c:\temp -ReportFileName ADSites.csv -CurrentForestName TailSpinToys.com This will export all AD sites within the TailSpinToys.com forest, to the c:\temp with the file name of: ADSites.csv .INPUTS Parameters: .OUTPUTS CSV file: $env:temp\ADSiteInfo.csv is the default. .NOTES General notes .ROLE Active Direoctory module needs to be available in the powershell session. #> [cmdletbinding()] param($ReportFilePath = "$env:temp", $ReportFileName = "ADSiteInfo.csv", $CurrentForestName = "Contoso.com" ) $ReportFile = $ReportFilePath + '\' + $ReportFileName Write-Verbose "`$ReportFilePath value is $ReportFilePath" Write-Verbose "`$ReportFileName value is $ReportFileName" Write-Verbose "`$ReportFile value is $ReportFile" Write-Verbose "`$CurrentForestName value is $CurrentForestName" Remove-item $ReportFile -ErrorAction SilentlyContinue #removes file if already exists $ThisString="Identity,RegionForServers,ADSite,RegionForClients" Add-Content "$ReportFile" $ThisString $ADForest = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext("Forest", $CurrentForestName) [array]$Sites=[System.DirectoryServices.ActiveDirectory.Forest]::GetForest($ADForest).sites $Sites #Displays sites to be collected. ForEach ($Site in $Sites) { $SiteName = $Site.Name $FinalVal=$SiteName Add-Content "$ReportFile" $FinalVal } If (Test-Path -Path $ReportFile) { Write-Host "File $ReportFile exists." -ForegroundColor Green } Else { Write-Host "File $ReportFile was not created." -ForegroundColor Red } } Function Set-AutoDiscoverSiteScopeExchangeServers { <# .SYNOPSIS Sets Autodiscover AD Site scope on all Exchange servers in an organization. .DESCRIPTION This function allows repeatable tasks of Set-ClientAccessService for all Exchange servers the value of AutoDiscoverSiteScope to leverage an AD Deployment site. Additional information can be found at the Exchange team blog post: Start-Process https://techcommunity.microsoft.com/t5/Exchange-Team-Blog/Exchange-Active-Directory-Deployment-Site/ba-p/604329 The usage of an Exchange AD deployment site is recommended to ensure no Outlook disruptions when installing/adding another Exchange server to an organization. Without the proper AD site design setup, enviornments can have certifiate pop-ups appear within Outlook, which is not a desired result. This function also excludes any 'deploy' named AD sites in the assignment of the autodiscovery site scope. Additional information about the Autodiscover process: Start-Process 'https://docs.microsoft.com/en-us/previous-versions/office/exchange-server-2007-technical-articles/bb332063(v=exchg.80)' .EXAMPLE Set-AutoDiscoverSiteScopeExchangeServers Set the Autodiscover site value of all Exchange servers in an AD Forest with excluding the default exlusion sites that match the search of *deploy*. .EXAMPLE Set-AutoDiscoverSiteScopeExchangeServers -DeployADSiteExlusion "*test*" Set the Autodiscover site value of all Exchange servers in an AD Forest and will exlude setting any searched names of *test* in the AD forest to the AutodiscoverSiteScope on Exchnage servers. .EXAMPLE Set-AutoDiscoverSiteScopeExchangeServers -ImportFile c:\temp\ADSite.csv -DeployADSiteExlusion "*test*" Set the Autodiscover site value of all Exchange servers in an AD Forest to the values listed in the CSV file, specifically based on their related regions and will exlude setting any searched names of *test* in the AD forest to the AutodiscoverSiteScope on the targetted Exchnage servers. .NOTES The default scope is all Exchnage servers in the entire organiztion. Option to import selected CSV file, to scope both region and servers to be targetted for update. .OUTPUTS Screen data. .COMPONENT Needs to be connected to an Exchange server remotely via PowerShell or using Exchange Management Shell. #> [cmdletbinding(SupportsShouldProcess=$true,ConfirmImpact='High',HelpUri='https://mikeoneill.blog/Exchange_AddIn/Set-AutoDSitesAllExchangeServers/')] param ([String]$DeployADSiteExclusion ="*deploy*", #This allows AD sites with deploy in the name anywhere, to be excluded from being assigned to Exchange Servers. Single value entry allowed. $ImportFile = $null #CSV file to be used if assigning regions or scoping AD sites per data centers. ) Write-Host "Values currently set on Exchange servers:" -ForegroundColor Yellow Get-ClientAccessService | Format-Table Name,AutoDiscoverSiteScope -Autosize if ([string]::IsNullOrWhiteSpace($ImportFile)){ $Servers = Get-ExchangeServer $AutoDSiteScope = (Get-ADSite | Where-Object {$_.name -notlike "$DeployADSiteExclusion"}).name Write-Verbose "`$Servers value is $Servers" Write-Verbose "`$AutoDSiteScope value is $AutoDSiteScope" Write-Verbose "`$DeployADSiteExclusion value is $DeployADSiteExclusion" Write-Host "The domain contains the following AD (excluding any $DeployADSiteExclusion) sites:" -ForegroundColor Green $AutoDSiteScope Write-Host "" ForEach ($Server in $Servers) { If ($pscmdlet.ShouldProcess($Server.Identity, "Setting AutoDiscover site scope")) { Set-ClientAccessServer $Server.Identity –AutoDiscoverSiteScope $AutoDSiteScope -WarningAction SilentlyContinue } } } else { $servers = Import-Csv $ImportFile | Select-Object RegionForServers, Identity | Where-Object {$_.servers -ne ""} $exchange = New-Object 'System.Collections.Generic.Dictionary[string,string[]]' $items = Import-Csv $ImportFile | Select-Object RegionForClients, ADsite foreach($item in $items) { if(-not ($exchange.ContainsKey($item.RegionForClients))) { $exchange.Add($item.RegionForClients, $item.adsite) } else { $exchange[$item.RegionForClients] += $item.adsite } } foreach($AutoDiscoverySiteScope in $exchange.Keys) { $keyValues = $exchange[$AutoDiscoverSiteScope] -join "," if(($servers | Where-Object RegionForServers -eq $AutoDiscoverSiteScope | measure-object).count -gt 0) { Write-Host ("Invoke commands for RegionForServer {0}" -f $AutoDiscoverSiteScope) foreach($server in $servers | Where-Object RegionForServers -eq $AutoDiscoverSiteScope) { Write-Host ("Writing -Identity {0} -AutoDiscoverSiteScope {1}" -f $server.Identity, $keyValues) If ($pscmdlet.ShouldProcess($Server.Identity, "Setting AutoDiscover site scope")) { Set-ClientAccessServer -Identity $Server.Identity –AutoDiscoverSiteScope $keyValues -WarningAction SilentlyContinue } } } } } Write-Host "Values now currently set on Exchange servers:" -ForegroundColor Green Get-ClientAccessServer -WarningAction SilentlyContinue | Format-Table Name,AutoDiscoverSiteScope -Autosize -Wrap } #end of function #endregion Set ADAutoD site #region Function Set-EaiMailAddressesForUsers Function Set-EaiMailAddressesForUsers { <# .SYNOPSIS Sets E-Mail addresses for a defined list of users, using a set list of values. .DESCRIPTION This fuction uses a CSV import file to add an SMTP address to users and sets the new one as the primary. This allows a defined scope of users to only add the new value as needed. The CSV file must have two (2) columns: alias,addnewemailaddress Bu defining the alias and the needed new email address, it allows a specific value and set of users to only need to be updated. .EXAMPLE Set-EAEMailAddressesForUsers -InputFile c:\temp\AddSmtpMakePrimaryProxy.csv Imports the file of c:\temp\AddSmtpMakePrimaryProxy.csv, and then sets the values listed in file. .EXAMPLE Set-EAEMailAddressesForUsers -InputFile c:\names\AddSmtpPrimary.csv -verbose Imports the file of c:\names\AddSmtpPrimary.csv, shows verbose content, and then sets the values listed in file. .EXAMPLE Set-EAEMailAddressesForUsers -InputFile c:\temp\AddSmtpMakePrimaryProxy.csv -WhatIf Imports the file of c:\temp\AddSmtpMakePrimaryProxy.csv and only runs in WhatIf mode with not setting any values to any aliases. .INPUTS CSV file is required. Needs 2 columns: alias and the new email address requested. Ensure the CSV file is NOT opened when trying to import into this function to run updates for users. .OUTPUTS Screen output of updated aliases .NOTES Built March 28, 2020 for specific add of e-mail new address and set as primary. Additional options may be built in later if needed. .FUNCTIONALITY This function is only designed to accept one new e-mail address currently, add it to the related alias, and set that new value as the default reply address. #> [cmdletbinding(SupportsShouldProcess=$true, HelpUri = 'https://mikeoneill.blog/exchange_addin/set-eaimailaddressesforusers/', ConfirmImpact='High')] param ( [parameter(HelpMessage="Enter computer name to run function againist.")] $InputFile = "c:\temp\AddSmtpMakePrimaryProxy.csv", [ValidateSet("MailContact", "MailNonUniversalGroup", "MailUniversalDistributionGroup","MailUniversalSecurityGroup","MailUser","PublicFolder","UserMailbox")] $MailObjectType) Begin { Write-Host "Starting the set process..." -ForegroundColor Yellow } Process { Write-Verbose "`InputFile array contains the value $InputFile" $InputFile | ForEach-Object { $userName = Import-Csv $InputFile | Select-Object alias Write-Verbose "`$UserName array contains $UserName" $user = Get-Recipient -Identity $userName.alias -RecipientType $MailObjectType Write-Verbose "`$User array contains $User" $AddNewemailAddress = Import-csv $InputFile | Select-Object AddNewEmailAddress Write-Verbose "`$AddNewemailAddress array contains: $AddNewemailAddress" $useremailaddresses = Get-Recipient -Identity $userName.alias -RecipientType $MailObjectType | Select-Object -ExpandProperty emailaddresses $useremailaddresses = $useremailaddresses -creplace 'SMTP', 'smtp' Write-Verbose "`$useremailaddresses array contains $useremailaddresses" $useremailaddresses += "SMTP:$($AddNewemailAddress.addnewemailaddress)" if ($pscmdlet.ShouldProcess("$user", "Setting $useremailaddresses")) { Update-Recipient -Identity $userName.alias -RecipientType $MailObjectType -emailAddresses $useremailaddresses } Write-Host "The user: $($user.alias) has the following addresses set;" -ForegroundColor Green Get-Recipient -Identity $userName.alias -RecipientType $MailObjectType | Format-List EmailAddresses, PrimarySmtpAddress } } end { Write-Host "Task complete for all current users in CSV file." -ForegroundColor Green } } #endregion Function Set-EaiMailAddressesForUsers <# For current listing of all SKU's Start-Process https://docs.microsoft.com/en-us/azure/active-directory/enterprise-users/licensing-service-plan-reference #> #region Get-EaiO365LicenseInfoWithPlans Viewing User values of direct assigned O365 licensing and plans within O365/M365 using graph endpoint Function Get-EaiO365LicenseInfoWithPlans { <# .Synopsis List out all licenses and service plans for a given user. .DESCRIPTION This script will list out all licenses and service plans for a given user. It will also list out the service plan ID, which is needed to enable or disable a service plan. This script requires the MsGraph PowerShell module. Permissions needed fro Graph access: User.Read.All .EXAMPLE Get-EaiO365LicenseInfoWithPlans -Identity "bob@contoso.com" This will list out each license sku, its' title, and the service plans assigned to the user, Bob, in the Contoso tenant. .EXAMPLE Get-EaiO365LicenseInfoWithPlans -Identity "mike@contoso.com" | Format-Table This will list out in a table format, each license sku, its' title, and the service plans assigned to the user, Mike, in the Contoso tenant. .EXAMPLE Get-EaiO365LicenseInfoWithPlans -unlimited This will list out, each license sku, its' title, and the service plans assigned to all users in the current tenant. .EXAMPLE Get-EaiO365LicenseInfoWithPlans -Identity "Diane@contoso.com" | Export-Csv -NoTypeInformation -Path c:\temp\plans.csv This will export a CSV file to C:\temp with the license sku, its' title, and the service plans assigned to the user, Diane, in the Contoso tenant. .INPUTS UserPrincipalName or 'unlimited' switch option for all users. .OUTPUTS Output results to screen. .NOTES Created by Mike O'Neill, August 28, 2023. Reference: Start-Process https://learn.microsoft.com/en-us/microsoft-365/enterprise/view-account-license-and-service-details-with-microsoft-365-powershell?view=o365-worldwide#to-view-services-for-a-user-account #> [cmdletbinding(HelpUri = 'https://mikeoneill.blog/exchange_addin/get-eaio365licenseinfowithplans/')] param( [Parameter()] [string]$Identity, [parameter()] [switch]$unlimited ) class Output { $UserPrincipalName $License $ServicePlanName $ProvisionStatus $AppliesTo $ServicePlanID } if ($Unlimited -eq $true){ $Users=Get-MgUser | Select-Object UserPrincipalName Write-Verbose "$Users is the value of the Users variable." } else { Write-Verbose "$Identity is the value of the Identity variable." $Users=Get-MgUser -UserId $Identity | Select-Object UserPrincipalName Write-Verbose "$Users is the value of the User variable." } Foreach ($User in $Users){ Write-Verbose "User: $User" $userUPN=$User.UserPrincipalName Write-Verbose "UserUPN: $userUPN" $allLicenses = Get-MgUserLicenseDetail -UserId $userUPN -Property SkuPartNumber, ServicePlans Write-Host Write-Host "User: $userUPN" -ForegroundColor Green Write-Host if ($null -eq $allLicenses){ Write-Host "No licenses assigned to user: $userUPN." -ForegroundColor Yellow Write-Host } $output=[Output]::new() $allLicenses | ForEach-Object { $output.UserPrincipalName="$($User.UserPrincipalName)" $output.License = "$($_.SkuPartNumber)" $_.ServicePlans | ForEach-Object { $output.ServicePlanName = "$($_.ServicePlanName)" $output.ProvisionStatus = "$($_.ProvisioningStatus)" $output.AppliesTo = "$($_.AppliesTo)" $output.ServicePlanID = "$($_.ServicePlanId)" $output } } } } #endregion Get-EaiO365LicenseInfoWithPlans Viewing User values of direct assigned O365 licensing and plans within O365/M365 using graph endpoint #region Get-EaiO365Photos function Function Get-EaiO365Photos { <# .SYNOPSIS Get and download photos from O365. .DESCRIPTION Designed to download photos from all users in O365 or to get information about a single user. This exports the images to a folder and lists out user objects with no photos. .EXAMPLE Get-EaiO365Photos -unlimited Downloads all photos from an O365 tenant and saves them into c:\Temp\Photo on the local machine by default. If the folder does not exist, it will create it. If users do not have photos, their alias is listed in 'UsersNoPhotos.csv' file in the defined path. .EXAMPLE Get-EaiO365Photos -unlimited -path c:\Temp Downloads all photos from an O365 tenant and saves them into c:\Temp on the local machine. If the folder does not exist, it will create it. If users do not have photos, their alias is listed in 'UsersNoPhotos.csv' file in the defined path. .EXAMPLE Get-EaiO365Photos -unlimited -path \\someserver\someshare Downloads photos from an O365 tenant and saves them into the UNC path of \\someserver\someshare remote location. If the folder does not exist, it will attempt to create it. If users do not have photos, their alias is listed in 'UsersNoPhotos.csv' file in the defined path. .EXAMPLE Get-EaiO365Photos -identity mike@contoso.com Download photo for user mike@contoso.com. If the user does not have a photo, their alias is listed in 'UsersNoPhotos.csv' file in the defined path. .EXAMPLE Get-EaiO365Photos -identity 'Mia@contoso.com', 'Tony@contoso.com' Download photo information for users Mia@contoso.com and Tony@contoso.com and outputs any user(s) without a photo in 'UsersNoPhotos.csv' file in the defined path. .INPUTS Path to save files to. .OUTPUTS Images in .jpg format and list of users without an image. .NOTES Original code from: Start-Process https://blog.jijitechnologies.com/how-to-download-office365-user-profile-photo Updated to function and added error checking information for easier usage. Updates assisted by: Tony Radkiewicz. .COMPONENT This uses Exchange online modern authentication cmdlet. PowerShell session must be logged onto the EXOv2 module with modern authentication connections. For information: Start-Process https://docs.microsoft.com/en-us/powershell/exchange/exchange-online-powershell-v2?view=exchange-ps For direct download: Install-Module -Name ExchangeOnlineManagement -force -allowclobber #> [cmdletbinding(DefaultParameterSetName='Single User', HelpUri = 'https://mikeoneill.blog/exchange_addin/get-eaio365photos/')] param ( $folderpath="C:\Temp\Photos\", [parameter(ParameterSetName='Unlimited')] [switch]$Unlimited, [parameter(ParameterSetName='Single User')] [string[]]$Identity ) try { $ExSession = @() $ExSession = Get-PSSession | Where-Object {(($_.name -match 'Exchangeonline*') -and ($_.computername -eq 'outlook.office365.com') -and ($_.configurationName -eq 'Microsoft.Exchange'))} if (($ExSession.count -eq 0) -xor ((Get-ConnectionInformation).TokenStatus -eq 'Active')){throw "No connected sessions"} } catch { Write-Host 'You are not connected to an EXOv2 or EXOv3 PowerShell session.' break } if (($null -eq $Identity) -and !($Unlimited)){ do { $Identity = Read-Host "Please enter an identity required" } until ($null -ne $Identity) } #Create class Class ListItem { [String]$alias } Write-Host "Retreiving mailboxes..." -ForegroundColor Cyan Write-Verbose "`$folderpath is $folderpath" [Array]$NoImageList = @() #region Confirm if requested path exists, if not, create it. if ((Test-Path $folderpath) -eq $false) { Write-Host "The folder located at $folderpath does not exist, creating it." -ForegroundColor Yellow New-Item -ItemType directory -Path $folderpath –force } else { Write-Host "The folder located at $folderpath exists..." -ForegroundColor Green } #endregion Confirm if requested path exists, if not, create it. #region Download profile pictures from Office 365 if ($Unlimited -eq $true){ $Users=Get-EXOMailbox -RecipientTypeDetails UserMailbox -ResultSize Unlimited | Select-Object UserPrincipalName,Alias Foreach($User in $Users) { $photo=Get-Userphoto -Identity $User.UserPrincipalName -ErrorAction SilentlyContinue Write-Verbose "`$photo contains $photo" If($null -ne $photo.PictureData) { $path=$folderpath+$User.Alias+".jpg" Write-Verbose "`$path is $path" [io.file]::WriteAllBytes($path,$photo.PictureData) Write-Verbose "`$user.alias is $user.alias" Write-Host $User.Alias 'profile picture downloaded.' -ForegroundColor Green } else { $ListItem = New-Object -TypeName ListItem -Property @{ Alias = $User.Alias } Write-Host $User.Alias 'has no profile picture.' -ForegroundColor Yellow Write-Verbose "`$user.alias is $user.alias" $NoImageList += $NoImageList + $ListItem } } } if ($null -ne $Identity){ Write-Verbose "`$Identity is $Identity" Foreach($User in $Identity) { Write-Verbose "`$user is $user" $UserObject = Get-Mailbox -RecipientTypeDetails UserMailbox -Identity $User | Select-Object UserPrincipalName,Alias Write-Verbose "`$UserObject is $UserObject" $photo=Get-Userphoto -Identity $User -ErrorAction SilentlyContinue Write-Verbose "`$photo contains $photo" If($null -ne $photo.PictureData) { $path=$folderpath+$UserObject.alias+".jpg" Write-Verbose "`$path is $path" [io.file]::WriteAllBytes($path,$photo.PictureData) Write-Verbose "`$userObject is $userObject" Write-Host $UserObject.alias "profile picture downloaded." -ForegroundColor Green } else { $ListItem = New-Object -TypeName ListItem -Property @{ Alias = $UserObject.alias } Write-Host $User 'has no profile picture.' -ForegroundColor Yellow Write-Verbose "`$userObject.alias is $userObject.alias" $NoImageList += $NoImageList + $ListItem } } } #endregion Download profile pictures from Office 365 #region CSV build of list of no photo users $OutputFile = $folderpath + 'UsersNoPhotos.csv' Write-Verbose "`$OutputFile is $OutputFile" Write-Verbose "`$NoImageList is $NoImageList" $NoImageList | Sort-Object alias | Select-Object alias | Get-Unique -AsString -OutVariable FinalFile | Out-Null $FinalFile | Export-CSV -NoTypeInformation $OutputFile Write-Host "Task complete." -ForegroundColor Green #endregion CSV build of list of no photo users } #endregion Get-EaiO365Photos function #region Get-EaiCalendarEventsOfRecurrenceItems Function Get-EaiCalendarEventsOfRecurrenceItems { <# .SYNOPSIS Get recurrence calendar events for a specific user. .DESCRIPTION While being logged onto MS Graph API, this function will list out recurrence meetings of specific user or users into an Excel file. Only the attendees listed for events that owned by the requested user will be listed. Logic written to exclude non-owner attendee events. .EXAMPLE Get-EaiCalendarEventsOfRecurrenceItems -UserUPN 'mike@contoso.com' -DaysBackwards 180 -DaysForwards 180 -OutputPath c:\temp2\ -verbose This will search the mailbox of Mike@contoso.com for a period of 180 days before and after the day this is run and output the file to the c:\temp2\ folder with verbose information displayed. .EXAMPLE Get-EaiCalendarEventsOfRecurrenceItems -UserUPN 'Diane@forthcoffee.com' -DaysBackwards 30 -DaysForwards 0 -OutputPath c:\temp\ This will search the mailbox of Diane@forthcoffee.com for a period of 30 days before until the day this is run and output the file to the c:\temp\ folder. .EXAMPLE Get-EaiCalendarEventsOfRecurrenceItems -UserUPN 'Sam@TailSpinToys.com','Chloe@TailSpinToys.com' -DaysBackwards 30 -DaysForwards 0 -OutputPath c:\temp\ This will search the mailboxes of Sam@TailSpinToys.com and Chloe@TailSpinToys.com for a period of 30 days before until the day this is run and output the file to the c:\temp\ folder. .INPUTS UserUPN to scan and date range for the query. .OUTPUTS CSV file in defined value. .ROLE Azure AD and Exchange online calendaring Application access needed: - User.Read.All - Calendars.Read .FUNCTIONALITY This function can list all attendees from a reoccuring meeting and using the domain parameter, can exlude all current domain attendees and only list non-domain ones. .NOTES Filter parameter value start-process https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter recurrance values start-process https://docs.microsoft.com/en-us/graph/api/resources/recurrencerange?view=graph-rest-1.0 Recurrence patterns https://docs.microsoft.com/en-us/graph/api/resources/recurrencepattern?view=graph-rest-1.0 #> [cmdletbinding(HelpUri = 'https://mikeoneill.blog/exchange_addin/get-eaicalendareventsofrecurrenceitems/')] Param( [parameter(Mandatory=$true)][array]$UserUPN, [parameter(Mandatory=$true)]$OutputPath, $domain, #Change this to user name to avoid $DaysForwards = 180, $DaysBackwards = 180 #add output date of event and need non-recurrence ) #collection class defined Class Result { [String]$Subject [String]$Attendee [String]$RecurrencePattern [String]$RecurrenceRange [string]$StartDate [string]$EndDate } #loop through multiple users foreach ($UserName in $UserUPN){ $startTime = (get-date).AddDays(-$DaysBackwards) $endTime = (get-date).AddDays($DaysForwards) $UserID = (Get-MgUser -userid $UserName).Id $UserMail = (Get-MgUser -UserId $UserName).Mail [array]$ResultList = @() #region get content #first call needed should be get-mgusercalendarevents $calEvents = Get-MgUserCalendarView -UserId $userId -StartDateTime $startTime -EndDateTime $endTime -All Write-Verbose "`$calEvents is $calEvents" #loop through all returned events foreach ($calEvent in $calEvents) { #parse ID Value from additional properties IF it's a recurring meeting Write-Verbose "`$calEvent is $calEvent" if($calEvent.AdditionalProperties.occurrenceId) { $eventId = (($calEvent.AdditionalProperties.occurrenceId).split("."))[1] Write-Verbose "`$eventID is $eventID" $eventDetails = Get-MgUserEvent -UserId $UserID -EventId $eventId Write-Verbose "`$eventDetails is $eventDetails" #record the data to arraylist foreach ($eventDetail in $eventDetails){ Write-Verbose "`$eventDetail is $eventDetail" $EmailAddresses = $eventDetail.Attendees.EmailAddress $EmailWithoutUserMailIncluded = $EmailAddresses | Where-Object {$_.address -notlike $userMail} if ($EmailWithoutUserMailIncluded.length -eq $EmailAddresses.length){ #if ($calEvent.IsOrganizer -eq $true){ $EmailAddresses = $eventDetail.Attendees.EmailAddress Write-Verbose "`$EmailAddresses is $EmailAddresses" foreach ($EmailAddress in $EmailWithoutUserMailIncluded) { if ($EmailAddress.Address -match $domain){ Write-Verbose "`$EmailAddress is $EmailAddress" $Results = New-Object -TypeName Result -Property @{ Subject = $eventDetail.Subject Attendee = $EmailAddress.address RecurrencePattern = $eventDetail.Recurrence.Pattern.Interval RecurrenceRange = $eventDetail.Recurrence.Range StartDate = $eventDetail.Start.DateTime EndDate = $eventDetail.End.DateTime} } $ResultList = $ResultList + $Results Write-Verbose "`$Results is $Results" Write-Verbose "`$ResultList is $ResultList" } } } } #endregion getting content #region Compile CSV file if ($null -ne $OutputPath){ Write-Verbose "`$OutputPath is $OutputPath" $OutputFile = $OutputPath + "ExportCalendarEvents_$UserName.csv" Write-Verbose "`$OutputFile is $OutputFile" #Takes the entire ControlList variable, ensures the values are unique. $FinalFile = $ResultList | Sort-Object Subject | select-object Subject, Attendee, StartDate, EndDate -Unique $FinalFile | Sort-Object Subject | ` Select-Object Subject, Attendee, StartDate, EndDate | ` Export-CSV -NoTypeInformation $OutputFile Write-Verbose "`$FinalFile is $FinalFinal" } #endregion Compile CSV file } #end of foreach username loop } }#end of function #endregion Get-EaiCalendarEventsOfRecurrenceItems #region remove stale devices Function Remove-EaiMobileStaleDevices { <# .Synopsis This function removes devices from an end user based on a time in days. .DESCRIPTION As devices become in a stale partnership, it is sometimes desired to remove them from the end users list. This fuction can be used for all users within a tenant or specific user or users. .EXAMPLE Remove-EaiMobileStaleDevices -Unlimited -days 60 This will remove all device partnerships older than 60 days for all users within an Exchange on premises organization. .EXAMPLE Remove-EaiMobileStaleDevices -Unlimited -days 30 -WhatIf This will show what devices would be removed older than 30 days for all users within an Exchange on premises organization. .EXAMPLE Remove-EaiMobileStaleDevices -UserName mike@contoso.com -days 90 This code will remove all device partnerships, older than 90 days, from the user 'mike@contoso.com' .EXAMPLE Remove-EaiMobileStaleDevices -UserName 'mike@contoso.com','Emma@contoso.com' -days 45 -verbose This code will remove all device partnerships, older than 45 days, from the users 'mike@contoso.com' and 'Emma@contoso.com' and show verbose information. .INPUTS Number of days in the past to scan for partnerships. .OUTPUTS Screen values .NOTES Initial working code: 10/10/2021 Jason A. Wright, HP support created inital code for device removal. Mike O'Neill, Microsoft CE updated into function. .COMPONENT Designed for Exchange on premises server connection. #> [cmdletbinding(DefaultParameterSetName='User', SupportsShouldProcess=$true, HelpUri = 'https://mikeoneill.blog/exchange_addin/remove-eaimobilestaledevices/', ConfirmImpact='High')] param ([Parameter(Mandatory=$true,HelpMessage='Enter the number of days back to which look for stale devices.')]$days, [Parameter(ParameterSetName='Unlimited')][switch]$Unlimited, [Parameter(ParameterSetName='User', HelpMessage="Enter user name or a list of user names separated by commas.")][string[]]$UserName) try { $ExSession = @() $ExSession = Get-PSSession | Where-Object {(($_.name -match 'Exchangeonline*') -and ($_.computername -eq 'outlook.office365.com') -and ($_.configurationName -eq 'Microsoft.Exchange'))} if (($ExSession.count -eq 0) -xor ((Get-ConnectionInformation).TokenStatus -eq 'Active')){throw "No connected sessions"} } catch { Write-Host 'You are not connected to an EXOv2 or EXOv3 PowerShell session.' -ForegroundColor Yellow Send-MailMessage break } if ($unlimited){ $MobileDevices = Get-MobileDevice -ResultSize unlimited | Where-Object {$_.LastSyncAttemptTime -lt (Get-Date).adddays(-$days)} } else { Write-Verbose "`$UserName is $UserName" foreach ($User in $UserName) { Write-Verbose "`$User is $User" $UserMobileDevice = Get-MobileDevice -Identity $User -ErrorAction SilentlyContinue | Where-Object {$_.LastSyncAttemptTime -lt (Get-Date).adddays(-$days)} if ($null -eq $UserMobileDevice){ Write-Host "$user does not have a mobile device in this date range." -ForegroundColor Yellow } $MobileDevices = $UserMobileDevice + $MobileDevices } } Write-Verbose "`$MobileDevices array contains $MobileDevices" foreach ($mobileDevice in $MobileDevices) { Write-Host "removing: " $MobileDevice if ($pscmdlet.ShouldProcess("$ModileDevice", "removing the partnership")) { Write-Host "Removing: " $MobileDevice Remove-MobileDevice ([string]$mobiledevice.Guid) -Confirm:$false } } } #endregion remove stale devices |