ExchangePowerShell.psm1
# ExPS (ExchangePowerShell) Powershell Module # Author: Pietro Ciaccio | LinkedIn: https://www.linkedin.com/in/pietrociaccio | Twitter: @PietroCiac # Pre-requisites: Exchange Server 2016 Exchange Management Shell function Test-ExPSExchangeModule() { <# .SYNOPSIS Checks if the powershell session has the Exchange 2016 powershell module loaded. .DESCRIPTION Checks if the powershell session has the Exchange 2016 powershell module loaded. #> [cmdletbinding()] Param ( ) Process { if (!($env:ExchangeInstallPath)) { throw "Exchange Server 2016 system variable ExchangeInstallPath missing." } if ($env:ExchangeInstallPath -notmatch '\\V15\\') { throw "Exchange Server 2016 powershell module not detected." } try { $cmd = $null; $cmd = get-command 'get-mailbox' -erroraction stop } catch { $invexpr = ". '$env:ExchangeInstallPath\bin\RemoteExchange.ps1'; Connect-ExchangeServer -auto -ClientApplication:ManagementShell" Invoke-Expression $invexpr } } } Test-ExPSExchangeModule function Start-ExPSTimer { <# .SYNOPSIS Displays a timer. Used for waiting. .DESCRIPTION Displays a timer. Used for waiting. .PARAMETER Seconds Specify the number of seconds to wait. #> [cmdletbinding()] Param ( [Parameter(mandatory=$true,valuefrompipelinebypropertyname=$true)][system.int32]$Seconds ) $current = $host.UI.RawUI.CursorPosition For ($i = $seconds; $i -ge 0 ;$i--) { #Start-Sleep -Seconds 1 #write-progress "Waiting" -Status "$i seconds" -PercentComplete $(($i / $seconds) * 100) write-host " " -NoNewline if (($seconds.tostring().length - $i.tostring().length) -gt 0) { for ($x = 0; $x -lt $seconds.tostring().length - $i.tostring().length; $x++) { write-host "0" -NoNewline -ForegroundColor Yellow } } write-host $i"s." -ForegroundColor yellow -nonewline start-sleep -s 1 [console]::setcursorposition($current.x,$current.y) } } function Test-ExPSExchangeServerIdentity { <# .SYNOPSIS Checks an Exchange 2016 servers identity. .DESCRIPTION Checks an Exchange 2016 servers identity. .PARAMETER Identity Specify the identity of the computer. This can be piped from Get-ExchangeServer or specified explicitly using a string. #> [cmdletbinding()] Param ( [Parameter(mandatory=$true,valuefrompipelinebypropertyname=$true)][PSCustomObject]$Identity ) Process { # Validate Exchange Server if ($input) { if ($input.objectcategory.name -ne "ms-Exch-Exchange-Server"){ throw "Unable to validate Exchange server identity." } else { $ExchangeServer = $null; $ExchangeServer = $input } } if (!($input)) { if ($identity.gettype().fullname -ne "System.String") { throw "Unable to use parameter 'Identity' of type '$($identity.gettype().fullname)'." } else { try { $ExchangeServer = $null; $ExchangeServer = Get-ExchangeServer -Identity $identity -erroraction stop } catch { throw $_.exception.message } } } write-host $ExchangeServer.fqdn.toupper() -ForegroundColor yellow if ($ExchangeServer.Admindisplayversion.major -ne "15") { write-warning "Exchange version '$($ExchangeServer.AdminDisplayVersion.major)' unsupported. Cmdlet supports Exchange major version 15. There may be issues." } return $ExchangeServer } } function Test-ExPSMailboxIdentity { <# .SYNOPSIS Checks an Exchange 2016 mailboxes identity. .DESCRIPTION Checks an Exchange 2016 mailboxes identity. .PARAMETER Identity Specify the identity of the mailbox. This can be piped from Get-Mailbox or specified explicitly using a string. #> [cmdletbinding()] Param ( [Parameter(mandatory=$true,valuefrompipelinebypropertyname=$true)][PSCustomObject]$Identity ) Process { # Validate Mailbox if ($input) { if ($input.recipienttype -ne "usermailbox"){ throw "Unable to validate mailbox identity." } else { $Mailbox = $null; $Mailbox = $input } } if (!($input)) { if ($identity.gettype().fullname -ne "System.String") { throw "Unable to use parameter 'Identity' of type '$($identity.gettype().fullname)'." } else { try { $Mailbox = $null; $Mailbox = get-mailbox -Filter "samaccountname -eq ""$identity""" -erroraction stop } catch { throw $_.exception.message } } } if ($($Mailbox | measure).count -gt 1) { throw "Too many search results found for samaccountname '$identity'." } write-host $Mailbox.samaccountname.toupper() -ForegroundColor yellow if ($Mailbox.Admindisplayversion.major -ne "15") { write-warning "Mailbox version unsupported. Cmdlet supports Exchange major version 15. There may be issues." } return $Mailbox } } function Test-ExPSDate { <# .SYNOPSIS Checks date format for cmdlets. .DESCRIPTION Checks date format for cmdlets #> [cmdletbinding()] Param ( [Parameter(mandatory=$true,valuefrompipelinebypropertyname=$false)][PSCustomObject]$Date ) Process { # validate date time if ($date.gettype().fullname -ne "System.DateTime") { try { $date = $date.tostring() if ($date -match "\D") { throw "DateTime and numerical value formats supported only." } switch ($date.length) { 7 {$date = "0" + $date} 13 {$date = "0" + $date} default {} } switch ($date.length) { 8 {$date = [datetime]::ParseExact($date,'ddMMyyyy',$null)} 14 {$date = [datetime]::ParseExact($date,'ddMMyyyyHHmmss',$null)} default {throw "Unsupported date provided. Must be ddMMyyyy or ddMMyyyyHHmmss. E.g. 15112019150329 for 15th November 2019 15:03:29."} } } catch { throw $_.exception.message } } if ($date.gettype().fullname -ne "System.DateTime") { throw "'Date' format is unsupported. This must be of format System.DateTime or a string in the format of ddMMyyyy or ddMMyyyyHHmmss." } return $Date } } function OutputObj ($identity,$user,$found,$type,$displayname,$objectguid,$distinguishedname,$Membership) { $Domain = $null if ($DistinguishedName) { $Domain = $DistinguishedName.substring($DistinguishedName.indexof('dc=',[StringComparison]"CurrentCultureIgnoreCase")+3,$DistinguishedName.length - $DistinguishedName.indexof('dc=',[StringComparison]"CurrentCultureIgnoreCase")-3) $Domain = $($Domain -replace ",dc=",".").toupper() } [PSCustomObject]@{ "Identity" = $Identity "User" = $user "Domain" = $Domain "Found" = $found "Type" = $Type "DisplayName" = $displayname "ObjectGUID" = $objectguid "DistinguishedName" = $distinguishedname "Membership" = $Membership } } function Get-ExPSPermUsers () { <# .SYNOPSIS Gets all users including nested users from groups. .DESCRIPTION Gets all users including nested users from groups. .Identity Specify the identity of the object. #> [cmdletbinding()] Param ( [Parameter(mandatory=$true,valuefrompipelinebypropertyname=$true)][string]$MBXSamaccountname, [Parameter(mandatory=$true,valuefrompipelinebypropertyname=$true)][PSCustomObject]$Identity, [Parameter(mandatory=$true,valuefrompipelinebypropertyname=$true)][PSCustomObject]$ADData, [Parameter(mandatory=$true,valuefrompipelinebypropertyname=$true)][PSCustomObject]$Type, [Parameter(mandatory=$false,valuefrompipelinebypropertyname=$true)][PSCustomObject]$Membership = $null ) Process { # Validate identity if ($input) { $Identity = $null; $Identity = $input } $IDObj = $null; if ($identity.gettype().fullname -eq "System.String") { switch -regex ($identity) { "^S-" { # SID try { $ADData | . { process { if ($identity -match $_.domainsid.value) { if ($_.domain -eq $env:USERDNSDOMAIN) { $IDObj = Get-ADObject -Filter {objectsid -eq $identity} -Properties samaccountname,displayname } else { $IDObj = Get-ADObject -Filter {objectsid -eq $identity} -Properties samaccountname,displayname -Server $_.target } } }} } catch {} } "^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$" { # GUID try { $IDObj = Get-ADObject $identity -Properties samaccountname,displayname } catch {} } "dc=\w+$" { # Distinguishedname try { $Domain = $null; $Domain = $identity.substring($identity.indexof('dc=',[StringComparison]"CurrentCultureIgnoreCase")+3,$identity.length - $identity.indexof('dc=',[StringComparison]"CurrentCultureIgnoreCase")-3) $Domain = $Domain -replace ",dc=","." if ($Domain -eq $env:USERDNSDOMAIN) { $IDObj = Get-ADObject $identity -Properties samaccountname,displayname } else { $IDObj = Get-ADObject $identity -Properties samaccountname,displayname -Server $Domain } } catch {} } default { # samaccountname try { $IDObj = Get-ADObject -Filter {samaccountname -eq $identity} -Properties samaccountname,displayname } catch {} } } } if ($IDObj) { switch ($IDObj.objectclass) { "group" { # Group $IDObj | Get-ADGroup -Properties members | select -ExpandProperty members | . { process { $Mems = $null if ($Membership -ne $null) { $Mems += $Membership + " > $($IDObj.samaccountname)" } else { $Mems = "$($IDObj.samaccountname)" } Get-ExPSPermUsers -MBXSamaccountname $MBXSamaccountname -Identity $_ -Type $Type -ADData $ADData -Membership $Mems }} } "user" { # User OutputObj $MBXSamaccountname $IDObj.samaccountname $true $Type $IDObj.displayname $IDObj.objectguid $IDObj.distinguishedname $Membership } default {} } } else { OutputObj $MBXSamaccountname $identity $false $Type $null $null $null $null } } } function Get-ExPSMailboxPermission { <# .SYNOPSIS Gets a list of user objects that have permissions on a mailbox. .DESCRIPTION Gets a list of user objects that have permissions on a mailbox. The cmdlet also expands any groups including nested groups. Permissions are collected from - - Full Access - Send As - Send on Behalf - Inbox - Calendar Please note, self and inherited permissions are excluded from the results. If no switches are used the cmdlet will get all permission types. .PARAMETER Identity Specify the identity of the mailbox. This can be piped from Get-Mailbox or specified explicitly using a string. .PARAMETER FullAccess Specify whether to get full access permissions. .PARAMETER SendAS Specify whether to get send as permissions. .PARAMETER SendOnBehalf Specify whether to get send on behalf permissions. .PARAMETER Inbox Specify whether to get Inbox permissions. .PARAMETER Calendar Specify whether to get Calendar permissions. #> [cmdletbinding()] Param ( [Parameter(mandatory=$true,valuefrompipelinebypropertyname=$true)][PSCustomObject]$Identity, [Parameter(mandatory=$false,valuefrompipelinebypropertyname=$true)][switch]$FullAccess, [Parameter(mandatory=$false,valuefrompipelinebypropertyname=$true)][switch]$SendAs, [Parameter(mandatory=$false,valuefrompipelinebypropertyname=$true)][switch]$SendOnBehalf, [Parameter(mandatory=$false,valuefrompipelinebypropertyname=$true)][switch]$Inbox, [Parameter(mandatory=$false,valuefrompipelinebypropertyname=$true)][switch]$Calendar ) Process { if ($FullAccess -eq $false -and $SendAs -eq $false -and $SendOnBehalf -eq $false -and $Inbox -eq $false -and $Calendar -eq $false ) { $FullAccess = $true $SendAs = $true $SendOnBehalf = $true $Inbox = $true $Calendar = $true } # Validate identity if ($input) { $Mailbox = $null; $Mailbox = $input | Test-ExPSMailboxIdentity } else { $Mailbox = $null; $Mailbox = Test-ExPSMailboxIdentity -Identity $Identity } # Getting AD environment information $ADData = $null; $ADData = @() # Check for domains $Domains = $null; try { $Domains = (Get-ADForest).domains | % {Get-ADDomain $_} | select dnsroot,distinguishedname,domainsid $Domains | . { process { $ADData += [PSCustomObject]@{ "DomainSID" = $_.domainsid "Domain" = $_.dnsroot "Target" = $_.dnsroot } }} } catch { throw "Unable to get AD forest or domain information." } # Check for trusts $Trusts = $null; $Domains = $null; try { $Trusts = Get-adtrust -Filter * -Properties trustpartner,target,securityidentifier | ? {$_.direction -match "^inbound$|^BiDirectional$"} | select trustpartner,target,securityidentifier $Trusts | . { process { $ADData += [PSCustomObject]@{ "DomainSID" = $_.securityidentifier "Domain" = $_.trustpartner "Target" = $_.target } }} } catch { throw "Unable to get AD trust information." } # Get full mailbox permissions try { if ($FullAccess) { $FullAccessRes = $null; $FullAccessRes = $Mailbox | Get-MailboxPermission -erroraction stop | ? {$_.isinherited -eq $false -AND $_.user.securityidentifier -notmatch "S-1-5-10" -AND $_.accessrights -match "fullaccess" -and $_.deny -match "false"} } } catch { write-warning "Issue getting full access mailbox permissions. $($_.exception.message)" } if ($FullAccessRes) { $FullAccessRes.user.securityidentifier.value | . { process { Get-ExPSPermUsers -MBXSamaccountname $mailbox.samaccountname -Identity $_ -Type "FullAccess" -ADData $ADData }} } # Get Send As permissions try { if ($SendAs){ $SendAS = $null; $SendASRes = $Mailbox | Get-ADPermission | ? {$_.extendedrights -match "send-as" -and $_.isinherited -match "false" -and $_.user.securityidentifier -notmatch "S-1-5-10" -and $_.deny -match "false"} } } catch { write-warning "Issue getting send as permissions. $($_.exception.message)" } if ($SendAsRes) { $SendAsRes.user.securityidentifier.value | . { process { Get-ExPSPermUsers -MBXSamaccountname $mailbox.samaccountname -Identity $_ -Type "SendAs" -ADData $ADData }} } # Get public delegates try { if ($SendOnBehalf){ $SOB = $null; $SOB = $Mailbox.grantsendonbehalfto } } catch { Write-Warning "Issue getting send on behalf permissions. $($_.exception.message)" } if ($SOB) { $SOB | . { process { Get-ExPSPermUsers -MBXSamaccountname $mailbox.samaccountname -Identity $_.objectguid.guid -Type "SendOnBehalf" -ADData $ADData }} } # Inbox permissions try { if ($Inbox) { $InboxRes = $null; $InboxRes = get-mailboxfolderpermission "$($Mailbox.primarysmtpaddress):\inbox" | ? {$_.user -notmatch "anonymous|default" -and $_.accessrights -ne "none"} } } catch { Write-Warning "Issue getting inbox folder permissions. $($_.exception.message)" } if ($InboxRes) { $InboxRes | . { process { if ($_.user.usertype -eq "internal") { Get-ExPSPermUsers -MBXSamaccountname $mailbox.samaccountname -identity $_.user.adrecipient.sid.value -Type "Inbox $($_.accessrights[0].tostring())" -ADData $ADData } else { OutputObj $Mailbox.samaccountname $_.user.displayname $false "Calendar $($_.accessrights[0].tostring())" $null $null $null $null } }} } # Calendar permissions try { if ($Calendar) { $CalendarRes = $null; $CalendarRes = get-mailboxfolderpermission "$($Mailbox.primarysmtpaddress):\calendar" | ? {$_.user -notmatch "anonymous|default" -and $_.accessrights -ne "none"} } } catch { Write-Warning "Issue getting inbox folder permissions. $($_.exception.message)" } if ($CalendarRes) { $CalendarRes | . { process { if ($_.user.usertype -eq "internal") { Get-ExPSPermUsers -MBXSamaccountname $mailbox.samaccountname -identity $_.user.adrecipient.sid.value -Type "Calendar $($_.accessrights[0].tostring())" -ADData $ADData } else { OutputObj $Mailbox.samaccountname $_.user.displayname $false "Calendar $($_.accessrights[0].tostring())" $null $null $null $null } }} } } } function Clear-ExPSExchangeLogs { <# .SYNOPSIS Clears Exchange Server 2016 logs older than a specified date. .DESCRIPTION Clears Exchange Server 2016 logs older than a specified date. Deletes files with extensions .log .blg and .etl. The cmdlet will determine the Exchange and IIS logging directories automatically. .PARAMETER Identity Specify the identity of the computer. This can be piped from Get-ExchangeServer or specified explicitly using a string. .PARAMETER Date Specify the date from which to clear logs. This can be of type Date.Time or a string in the format of ddMMyyyy or ddMMyyyyHHmmss. .EXAMPLE Get-ExchangeServer Server1 | Clear-ExPSExchangeLogs -Date (get-date).adddays(-30) This will clear logs older than 30 days on the Exchange server Server1. .EXAMPLE Clear-ExPSExchangeLogs -Identity Server2 -Date 01112019 This will clear logs older than the 1st November 2019 on the Exchange server Server2. #> [cmdletbinding()] Param ( [Parameter(mandatory=$true,valuefrompipelinebypropertyname=$true)][PSCustomObject]$Identity, [Parameter(mandatory=$true,valuefrompipelinebypropertyname=$false)][PSCustomObject]$Date ) Process { # Validate identity if ($input) { $ExchangeServer = $null; $ExchangeServer = $input | Test-ExPSExchangeServerIdentity } else { $ExchangeServer = $null; $ExchangeServer = Test-ExPSExchangeServerIdentity -Identity $Identity } # validate date time $Date = Test-ExPSDate -Date $Date write-host "Clearing logs older than '$($date.datetime)'" # Script blocks $Scriptblock = $null; $Scriptblock = [scriptblock]::Create(' Param($thisdate) try { $ExchangeLoggingPath = $null; $ExchangeLoggingPath = $env:exchangeinstallpath + "Logging" if (test-path $ExchangeLoggingPath) { $Files = $null $Files += [System.IO.Directory]::GetFiles($ExchangeLoggingPath,"*.log","AllDirectories") $Files += [System.IO.Directory]::GetFiles($ExchangeLoggingPath,"*.blg","AllDirectories") $Files += [System.IO.Directory]::GetFiles($ExchangeLoggingPath,"*.etl","AllDirectories") $IISLoggingPath = $null; try { $IISLoggingPath = (get-iissite).logfile.directory } catch { write-warning "Unable to determine IIS logging paths." } if ($IISLoggingPath) { $IISLoggingPath | . {process { $Files += [System.IO.Directory]::GetFiles([System.Environment]::ExpandEnvironmentVariables($_),"*.log","AllDirectories") } } } } else { write-warning "$ExchangeLoggingPath does not exist." } } catch { throw $_.exception.message } $scopecount = 0 $successcount = 0 $errorcount = 0 if ($Files) { $Files | . {process { if ($([System.IO.File]::GetLastWriteTime($_)) -lt $thisdate) { $scopecount += 1 try { [System.IO.File]::Delete($_) $successcount += 1 } catch { $errorcount += 1 } } } } } write-host "Total found: $(($Files | measure).count). Scoped for deletion: $scopecount. Success: $successcount. Failed: $errorcount" ') # Local or Remote execution try { $localhostname = (Get-WmiObject win32_computersystem).DNSHostName+"."+(Get-WmiObject win32_computersystem).Domain if ($localhostname -eq $ExchangeServer.fqdn) { try { $scriptblock.invoke($date) } catch { throw $_.exception.message } } else { try { Invoke-Command -ComputerName $($ExchangeServer.fqdn) -ScriptBlock $Scriptblock -ArgumentList $date -ErrorAction stop } catch { throw $_.exception.message } } } catch { throw $_.exception.message } write-host "Done." } } function Enable-ExPSMaintenanceMode { <# .SYNOPSIS Puts a Microsoft Exchange Server 2016 computer into maintenance mode. .DESCRIPTION Puts a Microsoft Exchange Server 2016 computer into maintenance mode. CmdLet will - - drain queues - restart transport services - redirect messages to a redirection server - move off active database copies to an available DAG member - suspend the cluster node - prevent database activation on the server - suspend passive copies - set all server component states to inactive .PARAMETER Identity Specify the identity of the computer. This can be piped from Get-ExchangeServer or specified explicitly using a string. .PARAMETER RedirectionTarget Specify the identity of the computer you wish to redirect pending messages to. .PARAMETER MoveActiveDatabaseCopies Specify whether to move active database copies to other DAG members, if possible. The default is false. #> [cmdletbinding()] Param ( [Parameter(mandatory=$true,valuefrompipelinebypropertyname=$true)][PSCustomObject]$Identity, [Parameter(mandatory=$false,valuefrompipelinebypropertyname=$false)][PSCustomObject]$RedirectionTarget, [Parameter(mandatory=$false,valuefrompipelinebypropertyname=$false)][boolean]$MoveActiveDatabaseCopies = $false ) Process { # Validate identity if ($input) { $ExchangeServer = $null; $ExchangeServer = $input | Test-ExPSExchangeServerIdentity } else { $ExchangeServer = $null; $ExchangeServer = Test-ExPSExchangeServerIdentity -Identity $Identity } # Validate Redirection Server $RedirectionServer = $null; if ($RedirectionTarget) { try { $RedirectionServer = Test-ExPSExchangeServerIdentity -Identity $RedirectionTarget } catch { throw $_.exception.message } } # Determine DAG membership $isDAGMember = $false try { $MailboxServer = $null; $MailboxServer = Get-MailboxServer -identity $Exchangeserver.fqdn -erroraction stop if ($($MailboxServer | measure).count -ne 1) { throw "$($($MailboxServer | measure).count) servers returned from query. Unable to continue." } if ($MailboxServer.DatabaseAvailabilityGroup -ne $null) { $isDAGMember = $true } } catch { throw $_.exception.message } # Draining queues Write-Host "Putting '$($ExchangeServer.fqdn.toupper())' into maintenance mode." if ($RedirectionServer){ Write-Host "Using '$($RedirectionServer.fqdn.toupper())' for message redirection." } Write-Host "Draining mail queues." try { Set-ServerComponentState -Identity $($ExchangeServer.fqdn) -Component HubTransport -State Draining -Requester Maintenance -erroraction stop } catch { throw $_.exception.message } # Restarting transport services Write-Host "Restarting MSExchangeTransport and MSExchangeFrontEndTransport services." $n = 0 Do { try { invoke-command -ComputerName $($ExchangeServer.fqdn) -scriptblock {"MSExchangeTransport","MSExchangeFrontEndTransport" | restart-service -WarningAction SilentlyContinue} -ErrorAction stop -WarningAction SilentlyContinue break } catch { $n++ write-host "WARNING: Issue restarting MSExchangeTransport and MSExchangeFrontEndTransport services. Waiting 60 seconds then retrying." -nonewline -ForegroundColor Yellow Start-ExPSTimer -Seconds 60 write-host " Retry attempt $n of 5." -ForegroundColor Yellow } if ($n -eq 5) { throw "Issue restarting MSExchangeTransport and MSExchangeFrontEndTransport services." } } while ($true) # Redirect messages if ($RedirectionServer) { Write-Host "Redirecting messages." try { Redirect-Message -Server $($ExchangeServer.fqdn) -Target $($RedirectionServer.fqdn) -confirm:$false -erroraction stop -WarningAction SilentlyContinue } catch { throw $_.exception.message } } # DAG members only if ($isDAGMember) { # Move active database copies off try { Write-Host "Setting DatabaseCopyActivationDisabledAndMoveNow to 'True'." Set-MailboxServer -Identity $($ExchangeServer.fqdn) -DatabaseCopyActivationDisabledAndMoveNow $True -erroraction Stop -confirm:$false } catch { throw $_.exception.message } # Move active copies immediately try { $actives = $null; $actives = Get-MailboxDatabaseCopyStatus *\$($ExchangeServer.name) | ? {$_.activecopy -eq $true} write-host "$($($actives | measure).count) active database copies found." if ($actives -and $MoveActiveDatabaseCopies) { write-host "Moving active databases to other DAG members." $actives | . { process { if ($($($($_ | . { process {(get-mailboxdatabase $_.databasename).servers}}) | measure).count) -lt 2) { Write-Warning "No other database copies exist. Unable to move active database copy." } else { $move = $null; $move = Get-mailboxdatabase $($_.databasename) | Move-ActiveMailboxDatabase -MountDialOverride lossless -SkipClientExperienceChecks -SkipMaximumActiveDatabasesChecks -confirm:$false -erroraction stop if ($move.status -ne "Succeeded") { throw "$($move.identity) mailbox database move issue." } } } } } } catch { throw $_.exception.message } # Suspend cluster node Write-Host "Suspending cluster node." try { invoke-command -ComputerName $($ExchangeServer.fqdn) -ScriptBlock { if ((Get-ClusterNode $($using:ExchangeServer.fqdn)).state -ne "Paused") { Suspend-ClusterNode $($using:ExchangeServer.fqdn) } } -ErrorAction Stop | out-null } catch { throw $_.exception.message } # Set activation policy to blocked try { Write-Host "Setting DatabaseCopyAutoActivationPolicy to 'Blocked'." Set-MailboxServer -Identity $($ExchangeServer.fqdn) -DatabaseCopyAutoActivationPolicy Blocked -erroraction Stop -confirm:$false } catch { throw $_.exception.message } # Suspend passive copies try { $Copies = $null; $Copies = Get-MailboxDatabaseCopyStatus *\$($ExchangeServer.name) | ? {$_.activecopy -eq $false} if ($Copies) { Write-Host "Suspending passive copies." $Copies | . { process { $_ | Suspend-MailboxDatabaseCopy -confirm:$false -erroraction stop } } } } catch { throw $_.exception.message } } # Complete maintenance mode try { Write-Host "Completing maintenance mode." Set-ServerComponentState -Identity $($ExchangeServer.fqdn) -Component ServerWideOffline -State Inactive -Requester Maintenance -erroraction stop } catch { throw $_.exception.message } write-host "Done." } } function Disable-ExPSMaintenanceMode { <# .SYNOPSIS Removes a Microsoft Exchange Server 2016 computer from maintenance mode. .DESCRIPTION Removes a Microsoft Exchange Server 2016 computer from maintenance mode. Cmdlet will - - set all server component states to active - resume the cluster node - enable database activation on the server - resume passive database copies - resume transport - restart transport services .PARAMETER Identity Specify the identity of the computer. This can be piped from Get-ExchangeServer or specified explicitly using a string. #> [cmdletbinding()] Param ( [Parameter(mandatory=$true,valuefrompipelinebypropertyname=$true)][PSCustomObject]$Identity ) Process { # Validate identity if ($input) { $ExchangeServer = $null; $ExchangeServer = $input | Test-ExPSExchangeServerIdentity } else { $ExchangeServer = $null; $ExchangeServer = Test-ExPSExchangeServerIdentity -Identity $Identity } # Determine DAG membership $isDAGMember = $false try { $MailboxServer = $null; $MailboxServer = Get-MailboxServer -identity $Exchangeserver.fqdn -erroraction stop if ($($MailboxServer | measure).count -ne 1) { throw "$($($MailboxServer | measure).count) servers returned from query. Unable to continue." } if ($MailboxServer.DatabaseAvailabilityGroup -ne $null) { $isDAGMember = $true } } catch { throw $_.exception.message } # Remove from maintenance mode try { Write-Host "Removing '$($ExchangeServer.fqdn.toupper())' from maintenance mode." Set-ServerComponentState -Identity $($ExchangeServer.fqdn) -Component ServerWideOffline -State Active -Requester Maintenance -erroraction stop } catch { throw $_.exception.message } # DAG members only if ($isDAGMember) { # Resume cluster node Write-Host "Resuming cluster node." try { invoke-command -ComputerName $($ExchangeServer.fqdn) -ScriptBlock { if ((Get-ClusterNode $($using:ExchangeServer.fqdn)).state -ne "Up") { Resume-ClusterNode $($using:ExchangeServer.fqdn) } } -ErrorAction Stop | out-null } catch { throw $_.exception.message } # Move active database copies on try { Write-Host "Setting DatabaseCopyActivationDisabledAndMoveNow to 'False'." Set-MailboxServer -Identity $($ExchangeServer.fqdn) -DatabaseCopyActivationDisabledAndMoveNow $false -erroraction Stop -confirm:$false } catch { throw $_.exception.message } # Set activation policy to unrestricted try { Write-Host "Setting DatabaseCopyAutoActivationPolicy to 'Unrestricted'." Set-MailboxServer -Identity $($ExchangeServer.fqdn) -DatabaseCopyAutoActivationPolicy Unrestricted -erroraction Stop -confirm:$false } catch { throw $_.exception.message } # Resume passive copies try { $Copies = $null; $Copies = Get-MailboxDatabaseCopyStatus *\$($ExchangeServer.name) | ? {$_.activecopy -eq $false} if ($Copies) { Write-Host "Resuming passive copies." $Copies | . { process { $_ | Resume-MailboxDatabaseCopy -confirm:$false -erroraction stop } } } } catch { throw $_.exception.message } } # Resume transport Write-Host "Resuming transport." try { Set-ServerComponentState -Identity $($ExchangeServer.fqdn) -Component HubTransport -State Active -Requester Maintenance -erroraction stop } catch { throw $_.exception.message } # Restarting transport services Write-Host "Restarting MSExchangeTransport and MSExchangeFrontEndTransport services." $n = 0 Do { try { invoke-command -ComputerName $($ExchangeServer.fqdn) -scriptblock {"MSExchangeTransport","MSExchangeFrontEndTransport" | restart-service -WarningAction SilentlyContinue} -ErrorAction stop -WarningAction SilentlyContinue break } catch { $n++ write-host "WARNING: Issue restarting MSExchangeTransport and MSExchangeFrontEndTransport services. Waiting 60 seconds then retrying." -nonewline -ForegroundColor Yellow Start-ExPSTimer -Seconds 60 write-host " Retry attempt $n of 5." -ForegroundColor Yellow } if ($n -eq 5) { throw "Issue restarting MSExchangeTransport and MSExchangeFrontEndTransport services." } } while ($true) write-host "Done." } } function Read-ExPSIMAPLogs { <# .SYNOPSIS Reads the IMAP4 logs of an Exchange 2016 server. .DESCRIPTION Reads the IMAP4 logs of an Exchange 2016 server and organises the results into a structured format. This cmdlet will only return log entries where the user field has been populated (the author found that load balancer health tests generated a lot of unwanted noise). Be careful when running this cmdlet because it is possible to consume a large amount of memory if there are many log files. It is recommended to scope your commands to smaller date time ranges. .PARAMETER Identity Specify the identity of the computer. This can be piped from Get-ExchangeServer or specified explicitly using a string. .PARAMETER Start Specify the start date from which to read logs. This can be of type Date.Time or a string in the format of ddMMyyyy or ddMMyyyyHHmmss. If no start date time is set then the cmdlet will get 1 hours worth of logs. .PARAMETER End Specify the end date from which to read logs. This can be of type Date.Time or a string in the format of ddMMyyyy or ddMMyyyyHHmmss. .EXAMPLE Get-ExchangeServer Server1 | Read-ExPSIMAPLogs -Start (get-date).addhours(-1) This will read the IMAP logs from Server1 from 1 hour ago to the current date time. .EXAMPLE Read-ExPSIMAPLogs -Identity Server2 -Start 15112019 -End 16112019150329 This will read the IMAP logs from Server2 between 15th November 2019 00:00:00 until 16th November 2019 15:03:29. #> [cmdletbinding()] Param ( [Parameter(mandatory=$true,valuefrompipelinebypropertyname=$true)][PSCustomObject]$Identity, [Parameter(mandatory=$false,valuefrompipelinebypropertyname=$true)][PSCustomObject]$Start, [Parameter(mandatory=$false,valuefrompipelinebypropertyname=$true)][PSCustomObject]$End ) Process { # Validate identity if ($input) { $ExchangeServer = $null; $ExchangeServer = $input | Test-ExPSExchangeServerIdentity } else { $ExchangeServer = $null; $ExchangeServer = Test-ExPSExchangeServerIdentity -Identity $Identity } # validate date times if ($Start) { $Start = Test-ExPSDate -Date $Start } else { $Start = (get-date).addhours(-1) } if ($End) { $End = Test-ExPSDate -Date $End } else { $End = get-date } if ($Start -gt $End) { throw "Start date time cannot be later than the end date time" } # Determine log path try { $logpath = $null; $logpath = ($ExchangeServer | get-imapsettings).logfilelocation } catch { throw $_.exception.message } # Script blocks $Scriptblock = $null; $Scriptblock = [scriptblock]::Create(' Param($thislogpath,$thisstart,$thisend) try { $Files = $null if (test-path $thislogpath) { $Files += [System.IO.Directory]::GetFiles($thislogpath,"*.log","AllDirectories") $Files | . { process { $LastWriteTime = $null; $LastWriteTime = [System.IO.File]::GetLastWriteTime($_) if ($LastWriteTime -ge $thisstart -and $LastWriteTime -le $thisend) { try { [System.IO.File]::ReadAllLines($_) | . { process { if ($_[0] -ne "#") { $_ | convertfrom-csv -Header dateTime,sessionId,seqNumber,sIp,cIp,user,duration,rqsize,rpsize,command,parameters,context | ? {$_.datetime -ne "datetime" -and $_.user -ne ""} } } } } catch { write-warning $_.exception.message } } } } } else { write-warning "$thislogpath does not exist." } } catch { throw $_.exception.message } ') # Local or Remote execution try { $localhostname = (Get-WmiObject win32_computersystem).DNSHostName+"."+(Get-WmiObject win32_computersystem).Domain if ($localhostname -eq $ExchangeServer.fqdn) { try { $scriptblock.invoke($logpath,$start,$end) } catch { throw $_.exception.message } } else { try { Invoke-Command -ComputerName $($ExchangeServer.fqdn) -ScriptBlock $Scriptblock -ArgumentList $logpath,$start,$end -ErrorAction stop } catch { throw $_.exception.message } } sleep -s 1; [System.GC]::Collect() } catch { throw $_.exception.message } } } function Read-ExPSPOPLogs { <# .SYNOPSIS Reads the POP3 logs of an Exchange 2016 server. .DESCRIPTION Reads the POP3 logs of an Exchange 2016 server and organises the results into a structured format. This cmdlet will only return log entries where the user field has been populated (the author found that load balancer health tests generated a lot of unwanted noise). Be careful when running this cmdlet because it is possible to consume a large amount of memory if there are many log files. It is recommended to scope your commands to smaller date time ranges. .PARAMETER Identity Specify the identity of the computer. This can be piped from Get-ExchangeServer or specified explicitly using a string. .PARAMETER Start Specify the start date from which to read logs. This can be of type Date.Time or a string in the format of ddMMyyyy or ddMMyyyyHHmmss. If no start date time is set then the cmdlet will get 1 hours worth of logs. .PARAMETER End Specify the end date from which to read logs. This can be of type Date.Time or a string in the format of ddMMyyyy or ddMMyyyyHHmmss. .EXAMPLE Get-ExchangeServer Server1 | Read-ExPSPOPLogs -Start (get-date).addhours(-1) This will read the POP logs from Server1 from 1 hour ago to the current date time. .EXAMPLE Read-ExPSPOPLogs -Identity Server2 -Start 15112019 -End 16112019150329 This will read the POP logs from Server2 between 15th November 2019 00:00:00 until 16th November 2019 15:03:29. #> [cmdletbinding()] Param ( [Parameter(mandatory=$true,valuefrompipelinebypropertyname=$true)][PSCustomObject]$Identity, [Parameter(mandatory=$false,valuefrompipelinebypropertyname=$true)][PSCustomObject]$Start, [Parameter(mandatory=$false,valuefrompipelinebypropertyname=$true)][PSCustomObject]$End ) Process { # Validate identity if ($input) { $ExchangeServer = $null; $ExchangeServer = $input | Test-ExPSExchangeServerIdentity } else { $ExchangeServer = $null; $ExchangeServer = Test-ExPSExchangeServerIdentity -Identity $Identity } # validate date times if ($Start) { $Start = Test-ExPSDate -Date $Start } else { $Start = (get-date).addhours(-1) } if ($End) { $End = Test-ExPSDate -Date $End } else { $End = get-date } if ($Start -gt $End) { throw "Start date time cannot be later than the end date time" } # Determine log path try { $logpath = $null; $logpath = ($ExchangeServer | get-popsettings).logfilelocation } catch { throw $_.exception.message } # Script blocks $Scriptblock = $null; $Scriptblock = [scriptblock]::Create(' Param($thislogpath,$thisstart,$thisend) try { $Files = $null if (test-path $thislogpath) { $Files += [System.IO.Directory]::GetFiles($thislogpath,"*.log","AllDirectories") $Files | . { process { $LastWriteTime = $null; $LastWriteTime = [System.IO.File]::GetLastWriteTime($_) if ($LastWriteTime -ge $thisstart -and $LastWriteTime -le $thisend) { try { [System.IO.File]::ReadAllLines($_) | . { process { if ($_[0] -ne "#") { $_ | convertfrom-csv -Header dateTime,sessionId,seqNumber,sIp,cIp,user,duration,rqsize,rpsize,command,parameters,context | ? {$_.datetime -ne "datetime" -and $_.user -ne ""} } } } } catch { write-warning $_.exception.message } } } } } else { write-warning "$thislogpath does not exist." } } catch { throw $_.exception.message } ') # Local or Remote execution try { $localhostname = (Get-WmiObject win32_computersystem).DNSHostName+"."+(Get-WmiObject win32_computersystem).Domain if ($localhostname -eq $ExchangeServer.fqdn) { try { $scriptblock.invoke($logpath,$start,$end) } catch { throw $_.exception.message } } else { try { Invoke-Command -ComputerName $($ExchangeServer.fqdn) -ScriptBlock $Scriptblock -ArgumentList $logpath,$start,$end -ErrorAction stop } catch { throw $_.exception.message } } sleep -s 1; [System.GC]::Collect() } catch { throw $_.exception.message } } } |