Category Archives: Exchange

Checking IIS Authentication Settings Before and After Changes

When maintaining Exchange Server or other IIS-based applications, configuration changes — such as cumulative updates, patches, or manual adjustments — can sometimes alter authentication settings without notice.
These changes may lead to unexpected login prompts, authentication failures, or inconsistent client access.

To help detect and document such changes, I use a simple but powerful PowerShell script that captures IIS authentication settings before and after any updates or modifications.

How the Script Works

The script queries IIS and exports the current authentication configuration to a CSV file.
After performing your updates (for example, applying an Exchange CU or modifying a virtual directory), you run the script again to generate a second CSV snapshot.

Each CSV contains detailed authentication settings for every relevant IIS site and virtual directory — including Windows, Basic, OAuth, and other authentication providers.

You can then use PowerShell’s built-in Compare-Object command to identify any differences between the two CSV files:

Compare-Object (Import-Csv .\Auth-Before.csv) (Import-Csv .\Auth-After.csv) -Property Site,Path,AuthType,Enabled

This comparison highlights exactly which authentication settings have changed, allowing administrators to verify that everything still aligns with best practices and organizational standards.

Why This Is Useful

Authentication misconfigurations can be subtle — and difficult to diagnose once users begin reporting access issues.
By documenting authentication settings before and after maintenance, you gain:

  • Full visibility into IIS authentication changes.
  • Quick detection of unexpected configuration drift.
  • Simplified troubleshooting after updates or patching.
  • A reliable audit trail for compliance and change management.

Practical Example

Before installing a new Exchange cumulative update, you run the script and save the output as Auth-Before.csv.
After the update completes, you run it again to create Auth-After.csv.
By comparing the two files, you instantly see whether any authentication methods were enabled, disabled, or modified — all without manually checking dozens of virtual directories.

n summary:
This PowerShell script provides a simple, reliable way to monitor and compare IIS authentication settings before and after system changes — helping administrators maintain consistent, secure configurations and avoid service disruptions.

<#
.SYNOPSIS
  Extended local snapshot of IIS Authentication and Extended Protection (EP) settings per site/app/vdir on an Exchange server.

.DESCRIPTION
  Scans all IIS sites, applications, and virtual directories, and collects:
    • Anonymous / Basic / Windows / Digest authentication status (enabled/disabled)
    • Windows authentication providers (NTLM / Negotiate order)
    • Basic authentication realm (if configured)
    • SSL flags (Require SSL / client certificate mode)
    • Extended Protection settings (tokenChecking, flags, SPNs)
    • (Optional) Exchange virtual directories (OWA, ECP, EWS, Autodiscover, MAPI, ActiveSync, OAB, Outlook Anywhere)

  Also performs a brief risk assessment regarding reverse proxy (e.g. NetScaler) compatibility with Extended Protection settings.

  Output is saved in C:\CNXA\IIS-Auth-Snapshots\<timestamp> as CSV, JSON, and SUMMARY.txt.

.PARAMETER OutputRoot
  Root output folder. Default: C:\CNXA\IIS-Auth-Snapshots

.PARAMETER IncludeExchangeVdirs
  Include Exchange virtual directories (OWA, ECP, EWS, Autodiscover, MAPI, ActiveSync, OAB, Outlook Anywhere) if corresponding cmdlets are available.
#>

[CmdletBinding()]
param(
  [string]$OutputRoot = 'C:\CNXA\IIS-Auth-Snapshots',
  [switch]$IncludeExchangeVdirs
)

begin {
  $ErrorActionPreference = 'Stop'
  Import-Module WebAdministration -ErrorAction Stop

  $timestamp = (Get-Date).ToString('yyyy-MM-ddTHH-mm-ss')
  $runRoot   = Join-Path $OutputRoot $timestamp
  New-Item -ItemType Directory -Path $runRoot -Force | Out-Null
  Write-Host ("Snapshot folder: {0}" -f $runRoot) -ForegroundColor Cyan

  function Get-AuthInfo([string]$Location, [string]$PathType) {
    $row = [ordered]@{
      Location  = $Location
      PathType  = $PathType   # Site / App / VDir
      anonymousAuthentication = $null
      basicAuthentication     = $null
      windowsAuthentication   = $null
      digestAuthentication    = $null
      windowsProviders        = $null
      basicRealm              = $null
      sslFlags                = $null
      ep_tokenChecking        = $null   # None/Allow/Require (IIS viser ofte: Off/Accept/Require)
      ep_flags                = $null   # None/Proxy/ProxyCohosting/NoServiceNameCheck
      ep_spns                 = $null   # Semi-kolon-separeret liste
      Risk_NetScaler_EP       = $null   # Heuristik: EP + sandsynlig proxy = risiko
      Error                   = $null
    }

    try {
      foreach($name in 'anonymousAuthentication','basicAuthentication','windowsAuthentication','digestAuthentication'){
        try {
          $state = (Get-WebConfigurationProperty -PSPath IIS:\ -Location $Location -Filter "system.webServer/security/authentication/$name" -Name enabled -ErrorAction Stop).Value
          $row[$name] = [bool]$state
        } catch { $row[$name] = $null }
      }
      # Providers for Windows auth
      try {
        $providers = (Get-WebConfiguration -PSPath IIS:\ -Location $Location -Filter 'system.webServer/security/authentication/windowsAuthentication/providers').Collection |
                     ForEach-Object { $_.Value }
        $row['windowsProviders'] = ($providers -join ',')
      } catch { $row['windowsProviders'] = $null }
      # Basic realm
      try {
        $realm = (Get-WebConfigurationProperty -PSPath IIS:\ -Location $Location -Filter 'system.webServer/security/authentication/basicAuthentication' -Name 'realm' -ErrorAction Stop).Value
        $row['basicRealm'] = $realm
      } catch { $row['basicRealm'] = $null }
      # SSL flags
      try {
        $flags = (Get-WebConfigurationProperty -PSPath IIS:\ -Location $Location -Filter 'system.webServer/security/access' -Name 'sslFlags' -ErrorAction Stop).Value
        $row['sslFlags'] = $flags
      } catch { $row['sslFlags'] = $null }

      # Extended Protection (under windowsAuthentication/extendedProtection)
      try {
        $epBase = 'system.webServer/security/authentication/windowsAuthentication/extendedProtection'
        $token  = (Get-WebConfigurationProperty -PSPath IIS:\ -Location $Location -Filter $epBase -Name 'tokenChecking' -ErrorAction Stop).Value
        $flags2 = (Get-WebConfigurationProperty -PSPath IIS:\ -Location $Location -Filter $epBase -Name 'flags' -ErrorAction Stop).Value
        $spnCol = (Get-WebConfiguration -PSPath IIS:\ -Location $Location -Filter ($epBase + '/spn')).Collection
        $spns   = @(); if($spnCol){ $spns = $spnCol | ForEach-Object { $_.Value } }
        $row['ep_tokenChecking'] = $token
        $row['ep_flags']         = $flags2
        $row['ep_spns']          = ($spns -join ';')
      } catch {
        # Hvis noderne ikke findes, lader vi felter være null
      }

      # Heuristik for risiko bag reverse proxy: Hvis WindowsAuth=On og EP tokenChecking er 'Require' eller flags indeholder 'Proxy', så marker risiko
      if($row.windowsAuthentication -and ($row.ep_tokenChecking -match 'Require' -or ($row.ep_flags -and $row.ep_flags -match 'Proxy'))){
        $row.Risk_NetScaler_EP = $true
      } else {
        $row.Risk_NetScaler_EP = $false
      }
    }
    catch { $row['Error'] = $_.Exception.Message }

    return [pscustomobject]$row
  }
}

process {
  $rows = @()

  # Sites
  $sites = Get-Website | Sort-Object Name
  foreach($s in $sites){
    $rows += Get-AuthInfo -Location $s.Name -PathType 'Site'

    # Apps
    $apps = Get-WebApplication -Site $s.Name -ErrorAction SilentlyContinue
    foreach($a in $apps){
      $loc = "$($s.Name)$($a.Path)"
      $rows += Get-AuthInfo -Location $loc -PathType 'App'

      # VDirs under app
      $vdirs = Get-WebVirtualDirectory -Site $s.Name -Application $a.Path -ErrorAction SilentlyContinue
      foreach($v in $vdirs){
        $loc2 = "$($s.Name)$($v.Path)"
        $rows += Get-AuthInfo -Location $loc2 -PathType 'VDir'
      }
    }

    # Root VDirs (hvis ingen apps)
    $rootVdirs = Get-WebVirtualDirectory -Site $s.Name -Application '/' -ErrorAction SilentlyContinue
    foreach($rv in $rootVdirs){
      $loc3 = "$($s.Name)$($rv.Path)"
      $rows += Get-AuthInfo -Location $loc3 -PathType 'VDir'
    }
  }

  # Gem IIS-auth + EP
  $csvPath  = Join-Path $runRoot 'iis-auth.csv'
  $jsonPath = Join-Path $runRoot 'iis-auth.json'
  $rows | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $csvPath
  $rows | ConvertTo-Json -Depth 5 | Set-Content -Path $jsonPath -Encoding UTF8
  Write-Host ("IIS auth skrevet: {0}" -f $csvPath) -ForegroundColor Green

  # Lidt opsummering og fokus på EP + velkendte Exchange-kataloger
  $summary = New-Object System.Text.StringBuilder
  [void]$summary.AppendLine("IIS Auth / Extended Protection Snapshot - " + $timestamp)
  [void]$summary.AppendLine("")

  $hot = $rows | Where-Object { $_.Risk_NetScaler_EP -eq $true }
  if($hot.Count -gt 0){
    [void]$summary.AppendLine("!! Potentielle problemer bag reverse proxy (EP Require/Proxy + WindowsAuth):")
    $hot | Select-Object Location,PathType,windowsAuthentication,ep_tokenChecking,ep_flags,sslFlags | Format-Table | Out-String | ForEach-Object { [void]$summary.Append($_) }
  } else {
    [void]$summary.AppendLine("Ingen åbenlyse EP/Proxy-risici fundet baseret på heuristik.")
  }

  $summaryPath = Join-Path $runRoot 'SUMMARY.txt'
  $summary.ToString() | Set-Content -Path $summaryPath -Encoding UTF8

  if($IncludeExchangeVdirs){
    if(Get-Command Get-OwaVirtualDirectory -ErrorAction SilentlyContinue){
      $ex = [ordered]@{}
      $ex.OWA  = Get-OwaVirtualDirectory              | Select-Object Server,Name,InternalUrl,ExternalUrl,FormsAuthentication,BasicAuthentication,WindowsAuthentication,OAuthAuthentication,RequireSSL
      $ex.ECP  = Get-EcpVirtualDirectory              | Select-Object Server,Name,InternalUrl,ExternalUrl,FormsAuthentication,BasicAuthentication,WindowsAuthentication,OAuthAuthentication,RequireSSL
      $ex.EWS  = Get-WebServicesVirtualDirectory      | Select-Object Server,Name,InternalUrl,ExternalUrl,BasicAuthentication,WindowsAuthentication,OAuthAuthentication,RequireSSL
      $ex.Auto = Get-AutodiscoverVirtualDirectory     | Select-Object Server,Name,InternalUrl,ExternalUrl,WindowsAuthentication,OAuthAuthentication,RequireSSL
      $ex.MAPI = Get-MapiVirtualDirectory             | Select-Object Server,Name,InternalUrl,ExternalUrl,IISAuthenticationMethods,RequireSSL
      $ex.AS   = Get-ActiveSyncVirtualDirectory       | Select-Object Server,Name,InternalUrl,ExternalUrl,BasicAuthentication,WindowsAuthentication,ClientCertAuth,ExternalAuthenticationMethods,RequireSSL
      $ex.OAB  = Get-OabVirtualDirectory              | Select-Object Server,Name,InternalUrl,ExternalUrl,RequireSSL
      $ex.OA   = Get-OutlookAnywhere                  | Select-Object Server,Name,ExternalHostname,InternalHostname,SSLOffloading,ExternalClientAuthenticationMethod,InternalClientAuthenticationMethod,IISAuthenticationMethods

      # Flatten for CSV
      $exRows = @()
      foreach($k in $ex.Keys){ if($null -ne $ex[$k]){ $exRows += $ex[$k] | Select-Object * } }
      if($exRows.Count -gt 0){
        $exCsv  = Join-Path $runRoot 'exchange-vdirs.csv'
        $exJson = Join-Path $runRoot 'exchange-vdirs.json'
        $exRows | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $exCsv
        $ex     | ConvertTo-Json -Depth 6 | Set-Content -Path $exJson -Encoding UTF8
        Write-Host ("Exchange VDirs skrevet: {0}" -f $exCsv) -ForegroundColor Green
      } else {
        Set-Content -Path (Join-Path $runRoot 'exchange-vdirs.txt') -Value 'No Exchange VDirs returned.'
      }
    } else {
      Set-Content -Path (Join-Path $runRoot 'exchange-vdirs.txt') -Value 'Exchange cmdlets not found; skipping.'
    }
  }
}

end {
  # no-op
}

Automating Exchange Autodiscover Configuration During Server Installations

I recently came across a brilliant PowerShell script from Jeff Guillet (MCSM | MVP) that solves a problem many Exchange administrators know all too well — incorrect Autodiscover configurations being pushed to clients after a new server installation.
This issue often leads to certificate errors, Outlook connection issues, and unnecessary troubleshooting. Jeff’s script automates the entire process, ensuring a seamless experience for both users and admins.

The Challenge

When deploying new Microsoft Exchange servers, it’s easy for clients to receive outdated or incorrect Autodiscover URLs. This happens because new servers may register default service connection points (SCPs) before administrators have a chance to manually configure them.
As a result, Outlook clients can encounter certificate warnings or connect to the wrong Exchange instance — especially in coexistence or migration environments.

About the Script

Jeff Guillet’s PowerShell script automatically handles this configuration work for you. It monitors Active Directory and updates all relevant settings as soon as a new Exchange server is detected. This ensures your environment stays consistent and users never experience Autodiscover-related issues.

When a new server is installed, the script:

  • Updates the Autodiscover Service Connection Point (SCP) in Active Directory.
  • Configures the Outlook Anywhere (RPC over HTTP) FQDNs.
  • Sets the correct virtual directory URLs (EWS, OAB, ECP, MAPI, etc.) based on your preferred domain names.

This automation prevents Autodiscover from pointing to the wrong server and eliminates common certificate mismatch errors.

Why It Matters

Maintaining consistent Autodiscover and virtual directory configurations across all Exchange servers is essential for a stable environment. With this script in place, administrators can:

  • Prevent Outlook certificate pop-ups and connection failures.
  • Automate repetitive post-installation tasks.
  • Simplify coexistence during Exchange upgrades or migrations.
  • Ensure a reliable user experience with minimal manual intervention.

Real-World Benefits

For organizations managing hybrid or multi-server Exchange environments, Jeff’s script is a huge time-saver. It ensures every newly installed Exchange server immediately conforms to your organization’s configuration standards — all without lifting a finger.

By automating these critical steps, you reduce human error, improve reliability, and maintain consistent connectivity across the board.

In summary:
This PowerShell script by Jeff Guillet (MCSM | MVP) automatically updates the Autodiscover SCP, Outlook Anywhere FQDNs, and Exchange virtual directory URLs as new servers are deployed — helping administrators avoid certificate errors and streamline their deployment workflow.

For more information or to download the script, visit Jeff’s blog at www.expta.com or contact him directly at [email protected] / @expta on Twitter.

<#
.SYNOPSIS
Sets the Autodiscover service connection point (SCP) in Active Directory, Outlook Anywhere FQDNs, and virtual directory URLs for new Exchange servers as they are being installed.

Author/Copyright:    Jeff Guillet, MCSM | MVP - All rights reserved
Email/Blog/Twitter:  [email protected] | www.expta.com | @expta

THIS CODE IS MADE AVAILABLE AS IS, WITHOUT WARRANTY OF ANY KIND. THE ENTIRE RISK OF THE USE OR THE RESULTS FROM THE USE OF THIS CODE REMAINS WITH THE USER.

.NOTES
Version 1.0, October 7, 2015
Version 2.0, July 22, 2016

Revision History
---------------------------------------------------------------------
1.0	Initial release
1.1	Updated online link; Added code to install RSAT-AD-PowerShell if required
2.0	Major update:
		Made setting the new values easier by cloning an existing server
		Now also configures Outlook Anywhere and Exchange virtual directory internal and external URLs
		Revised verbiage and use *-ClientAccessService cmdlets for Exchange 2016
		Added -Verbose output to display the values we're configuring
		Improved overall display output
	
.DESCRIPTION
Sets the Autodiscover service connection point (SCP) in Active Directory, Outlook Anywhere FQDNs, and virtual directory URLs for new Exchange servers as they are being installed.

Exchange setup always configures the new SCP with the FQDN of the server which throws certificate warnings in Outlook because the self-signed Exchange certificate is not trusted. Read https://blogs.technet.microsoft.com/exchange/2015/11/18/exchange-active-directory-deployment-site for more information about this behavior.

This script should be run from an existing Exchange server of the same version, and is designed to be run while the new Exchange server is being installed. It loops until it finds an existing SCP for the target server and then configures it to match the same SCP and virtual directory URL values from the server to clone.

.PARAMETER Server
Specifies the Exchange 2010/2013/2016 server to configure.

.PARAMETER ServerToClone
Specifies the Exchange 2010/2013/2016 server to use for reference. The SCP, Outlook Anywhere, and internal/external URL values will be copied from this server to the target server.

.PARAMETER DomainController
Query and set on the specified domain controller, otherwise uses the current binding DC.

.LINK
http://www.expta.com/2016/07/new-set-autodiscoverscp-v2-script-is-on.html

.EXAMPLE
PS C:\>Set-AutodiscoverSCP.ps1 -Server exch02 -ServerToClone exch01

This command continually queries the current configuration domain controller until it finds an SCP for server EXCH02 and then sets it to match the SCP of EXCH01. It also configures Outlook Anywhere and the internal/external virtual directory URLs to match those found on EXCH01.

.EXAMPLE
PS C:\>Set-AutodiscoverSCP.ps1 -Server exch02 -ServerToClone exch01 -DomainController dc03

This command is almost the same as the command in the previous example, except it continually queries DC03 for the SCP record and configures it on that domain controller. This is useful when configuring a new Exchange server in a different Active Directory site.
#>

# Define the script parameters
Param (
	[CmdletBinding()]
	[Parameter(Position=1,Mandatory=$true)]
	[string]$Server,
	[Parameter(Position=2,Mandatory=$true)]
	[string]$ServerToClone,
	[Parameter(Position=3,Mandatory=$false)]
	[string]$DomainController
)

Process {
	# Validate the target server
	$ErrorActionPreference = "SilentlyContinue"
	$Server = $Server.ToUpper()
	$Ping = New-Object System.Net.NetworkInformation.Ping
	$Reply = $Ping.Send($Server).Status
	if ($Reply -ne "Success") {
		Write-Host "ERROR: $Server is not online or is not a valid server name." -Foreground Red
		Exit(1)
	}

	# Validate the server to clone
	$ServerToClone = $ServerToClone.ToUpper()
	$Reply = $null
	$Reply = $Ping.Send($ServerToClone).Status
	if ($Reply -ne "Success") {
		Write-Host "ERROR: $ServerToClone is not online or is not a valid server name." -Foreground Red
		Exit(1)
	}

	# Select the Domain Controller to run against
	if ($DomainController) { 
		if ((Get-WindowsFeature RSAT-AD-PowerShell).InstallState -eq "Available") {
			Add-WindowsFeature RSAT-AD-PowerShell
		}
		Import-Module ActiveDirectory
		$Error.Clear()
		$DomainController = (Get-ADDomainController $DomainController).HostName
		$DomainController = $DomainController.ToUpper()
		If ($Error) { 
			Write-Host "ERROR: $DomainController is not online or is not a valid domain controller." -Foreground Red
			Exit(1)
		}
	}
	else {
		$DomainController = (Get-ADServerSettings).DefaultConfigurationDomainController.Domain
	}

	# Discover where the PSSession is established and show Exchange version warning
	$PSSessionServer = Get-ExchangeServer (Get-PSSession | Where-Object {$_.State -eq 'Opened'}).ComputerName
	if ($PSSessionServer.AdminDisplayVersion.Major -eq 14) { $ExVersion = "2010" }
	if ($PSSessionServer.AdminDisplayVersion.Major -eq 15 -and $PSSessionServer.AdminDisplayVersion.Minor -eq 0) { $ExVersion = "2013" }
	if ($PSSessionServer.AdminDisplayVersion.Major -eq 15 -and $PSSessionServer.AdminDisplayVersion.Minor -eq 1) { $ExVersion = "2016" }
	if ($ExVersion -eq $null) {
		Write-Host "ERROR: This script must be run from the Exchange Management Shell on an Exchange 2010-2016 server." -Foreground Red
		Exit(1)
	}
	elseif ($ExVersion -eq "2010") {
		Write-Host "This script is currently running in an Exchange 2010 PowerShell session. Make sure $Server is installing Exchange 2010." -Foreground White -BackGround Red
		if ((Get-OwaVirtualDirectory -Server $ServerToClone -DomainController $DomainController -ADPropertiesOnly).InternalUrl.AbsoluteUri -eq $null) {
			Write-Host "ERROR: $ServerToClone is a higher version of Exchange than this server." -Foreground Red
			Exit(1)
		}
		Write-Host "NOTE: If you're installing your first Exchange 2013/2016 server in this environment you should run this script from that server while setup is running."
		Write-Host
	}
	else {
		Write-Host "This script is currently running in an Exchange $ExVersion PowerShell session. Make sure $Server is installing Exchange 2013 or later." -Foreground White -BackGround Red
		Write-Host
	}
	$ErrorActionPreference = "Continue"

	# Get the SCP, Outlook Anywhere, and virtual directory URL values from $ServerToClone
	Write-Host -NoNewline "Gathering SCP, Outlook Anywhere, and Exchange virtual directory values from $ServerToClone... " -Foreground White
	$SCPValue = (Get-ClientAccessServer $ServerToClone -DomainController $DomainController -ErrorAction SilentlyContinue -WarningAction SilentlyContinue).AutoDiscoverServiceInternalUri.AbsoluteUri
	$EasInternal = (Get-ActiveSyncVirtualDirectory -Server $ServerToClone -DomainController $DomainController -ADPropertiesOnly).InternalUrl.AbsoluteUri
	$EasExternal = (Get-ActiveSyncVirtualDirectory -Server $ServerToClone -DomainController $DomainController -ADPropertiesOnly).ExternalUrl.AbsoluteUri
	$EcpInternal = (Get-EcpVirtualDirectory -Server $ServerToClone -DomainController $DomainController -ADPropertiesOnly).InternalUrl.AbsoluteUri
	$EcpExternal = (Get-EcpVirtualDirectory -Server $ServerToClone -DomainController $DomainController -ADPropertiesOnly).ExternalUrl.AbsoluteUri
	If ($ExVersion -ne "2010") {
		$MapiInternal = (Get-MapiVirtualDirectory -Server $ServerToClone -DomainController $DomainController -ADPropertiesOnly).InternalUrl.AbsoluteUri
		$MapiExternal = (Get-MapiVirtualDirectory -Server $ServerToClone -DomainController $DomainController -ADPropertiesOnly).ExternalUrl.AbsoluteUri
	}
	$OabInternal = (Get-OabVirtualDirectory -Server $ServerToClone -DomainController $DomainController -ADPropertiesOnly).InternalUrl.AbsoluteUri
	$OabExternal = (Get-OabVirtualDirectory -Server $ServerToClone -DomainController $DomainController -ADPropertiesOnly).ExternalUrl.AbsoluteUri
	$OwaInternal = (Get-OwaVirtualDirectory -Server $ServerToClone -DomainController $DomainController -ADPropertiesOnly).InternalUrl.AbsoluteUri
	$OwaExternal = (Get-OwaVirtualDirectory -Server $ServerToClone -DomainController $DomainController -ADPropertiesOnly).ExternalUrl.AbsoluteUri
	$EwsInternal = (Get-WebServicesVirtualDirectory -Server $ServerToClone -DomainController $DomainController -ADPropertiesOnly).InternalUrl.AbsoluteUri
	$EwsExternal = (Get-WebServicesVirtualDirectory -Server $ServerToClone -DomainController $DomainController -ADPropertiesOnly).ExternalUrl.AbsoluteUri
	$OaInternal = (Get-OutlookAnywhere -Server $ServerToClone -DomainController $DomainController -AdPropertiesOnly).InternalHostname.HostnameString
	$OaExternal = (Get-OutlookAnywhere -Server $ServerToClone -DomainController $DomainController -AdPropertiesOnly).ExternalHostname.HostnameString
	Write-Host "Done!" -Foreground White

	# Verbose output shows cloned values
	Write-Verbose "SCP -  $SCPValue"
	Write-Verbose "EAS -  $EasInternal | $EasExternal"
	Write-Verbose "ECP -  $EcpInternal | $EcpExternal"
	Write-Verbose "MAPI - $MapiInternal | $MapiExternal"
	Write-Verbose "OAB -  $OabInternal | $OabExternal"
	Write-Verbose "OWA -  $OwaInternal | $OwaExternal"
	Write-Verbose "EWS -  $EwsInternal | $EwsExternal"
	Write-Verbose "OA -   $OaInternal | $OaExternal"

	# Check if we're running this script from the target server
	if ([System.Net.Dns]::GetHostByName($Server).HostName -eq [System.Net.Dns]::GetHostByName(($env:computerName)).HostName) {
		Write-Host
		Write-Host "NOTE: You are running this script from the same server you're configuring. If you're running it while installing Exchange the script may stall during configuration since setup restarts IIS and web services several times. If that happens simply CTRL-C and restart the script." -Foreground Yellow
		Write-Host
	}

	# Continually query AD for SCP value for $Server
	do {
		if ($ExVersion -eq "2016") {
			$SCP = (Get-ClientAccessService $Server -DomainController $DomainController -ErrorAction SilentlyContinue).AutoDiscoverServiceInternalUri.AbsoluteUri
		}
		else {
			$SCP = (Get-ClientAccessServer $Server -DomainController $DomainController -ErrorAction SilentlyContinue).AutoDiscoverServiceInternalUri.AbsoluteUri
		}
		$PercentComplete++
		if ($PercentComplete -eq 101) { $PercentComplete = 1 }
		Write-Progress -Activity "Searching for the SCP value for Exchange server $Server in Active Directory..." -PercentComplete $PercentComplete -Status "Please wait."
	}
	until ($SCP -ne $null)

	# Set the new SCP value in Active Directory
	$Error.Clear()
	if ($ExVersion -eq "2016") {
		Set-ClientAccessService $Server -AutoDiscoverServiceInternalUri $SCPValue -DomainController $DomainController
	}
	else {
		Set-ClientAccessServer $Server -AutoDiscoverServiceInternalUri $SCPValue -DomainController $DomainController
	}
	If ($Error) { Exit(1) }
	Write-Host "Setting SCP value for $Server to $SCPValue" -Foreground Green

	# Set the internal and external URLs for all virtual directories
	Write-Host -NoNewLine "Setting ActiveSyncVirtualDirectory internal and external URLs... " -Foreground Cyan
	Get-ActiveSyncVirtualDirectory -Server $Server -DomainController $DomainController -ADPropertiesOnly | Set-ActiveSyncVirtualDirectory -InternalUrl $EasInternal -ExternalUrl $EasExternal -DomainController $DomainController -WarningAction SilentlyContinue
	Write-Host "Done!" -Foreground Cyan
	Write-Host -NoNewLine "Setting EcpVirtualDirectory internal and external URLs... " -Foreground Cyan
	Get-EcpVirtualDirectory -Server $Server -DomainController $DomainController -ADPropertiesOnly | Set-EcpVirtualDirectory -InternalUrl $EcpInternal -ExternalUrl $EcpExternal -DomainController $DomainController -WarningAction SilentlyContinue
	Write-Host "Done!" -Foreground Cyan
	If ($MapiInternal -ne $null) {
		Write-Host -NoNewLine "Setting MapiVirtualDirectory internal and external URLs... " -Foreground Cyan
		Get-MapiVirtualDirectory -Server $Server -DomainController $DomainController -ADPropertiesOnly | Set-MapiVirtualDirectory -InternalUrl $MapiInternal -ExternalUrl $MapiExternal -DomainController $DomainController -WarningAction SilentlyContinue
		Write-Host "Done!" -Foreground Cyan
	}
	Write-Host -NoNewLine "Setting OabVirtualDirectory internal and external URLs... " -Foreground Cyan
	Get-OabVirtualDirectory -Server $Server -DomainController $DomainController -ADPropertiesOnly | Set-OabVirtualDirectory -InternalUrl $OabInternal -ExternalUrl $OabExternal -DomainController $DomainController -WarningAction SilentlyContinue
	Write-Host "Done!" -Foreground Cyan
	Write-Host -NoNewLine "Setting OwaVirtualDirectory internal and external URLs... " -Foreground Cyan
	Get-OwaVirtualDirectory -Server $Server -DomainController $DomainController -ADPropertiesOnly | Set-OwaVirtualDirectory -InternalUrl $OwaInternal -ExternalUrl $OwaExternal -DomainController $DomainController -WarningAction SilentlyContinue
	Write-Host "Done!" -Foreground Cyan
	Write-Host -NoNewLine "Setting WebServicesVirtualDirectory internal and external URLs... " -Foreground Cyan
	Get-WebServicesVirtualDirectory -Server $Server -DomainController $DomainController -ADPropertiesOnly | Set-WebServicesVirtualDirectory -InternalUrl $EwsInternal -ExternalUrl $EwsExternal -DomainController $DomainController -WarningAction SilentlyContinue
	Write-Host "Done!" -Foreground Cyan
	Write-Host -NoNewLine "Setting Outlook Anywhere FQDNs... " -Foreground White
	$OA = Get-OutlookAnywhere -Server $Server -DomainController $DomainController -AdPropertiesOnly
	If ($ExVersion -ne "2010") {
		$OA | Set-OutlookAnywhere -InternalHostname $OaInternal -InternalClientsRequireSsl $OA.InternalClientsRequireSsl -InternalClientAuthenticationMethod $OA.InternalClientAuthenticationMethod -ExternalHostname $OaExternal -ExternalClientsRequireSsl $OA.ExternalClientsRequireSsl -ExternalClientAuthenticationMethod $OA.ExternalClientAuthenticationMethod -DomainController $DomainController -WarningAction SilentlyContinue
	}
	else {
		$OA | Set-OutlookAnywhere -ExternalHostname $OAExternal -DomainController $DomainController -WarningAction SilentlyContinue
	}
	Write-Host "Done!" -Foreground White
	Write-Host
	Write-Host "Be sure to install and enable the same trusted SSL certificate that's configured on $Server to complete configuration." -Foreground Red
}

Installation of New Exchange server

If you ever have installed a new Exchange server in an Active Directory orgainisation that allready have an Exchange server (That’s not so often anymore), you may have discovered that users get Certificate warning in Outlook, unto you have changed the URL’s to match the certificates.

But I found this script on Github: https://gist.github.com/kevinblumenfeld/31b03bf439b1f94f460eb754ef74120e

It copy the settings of all IIS url’s to the new Exchange server, when it found it in Active Directory.
So run this script before the start of the installation, and you will not receive the certificate warnings after the installation.

Add Exchange management Shell to “normal powershell

Here are a script, that you can include in yours script, to add Exchange management shell, to “normal” powershell scripts.

$StopWatch = [System.Diagnostics.StopWatch]::StartNew()
Function Test-Command ($Command)
{
    Try
    {
        Get-command $command -ErrorAction Stop
        Return $True
    }
    Catch [System.SystemException]
    {
        Return $False
    }
}

IF (Test-Command "Get-Mailbox") {Write-Host "Exchange cmdlets already present"}
Else {

    $CallEMS = ". '$env:ExchangeInstallPath\bin\RemoteExchange.ps1'; Connect-ExchangeServer -auto -ClientApplication:ManagementShell "

    Invoke-Expression $CallEMS
$stopwatch.Stop()
$msg = "`n`nThe script took $([math]::round($($StopWatch.Elapsed.TotalSeconds),2)) seconds to execute..."
Write-Host $msg
$msg = $null
$StopWatch = $null
}

Move from Office 365 to Onpremise in a Hybrid enviroment

import-module msonline
(install-module msonline)

$O365CREDS = Get-Credential
$ONPREMCREDS = Get-Credential #used UPN Username

$SESSION = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell -Credential $O365CREDS -Authentication Basic -AllowRedirection

Import-PSSession $SESSION
Connect-MsolService -Credential $O365CREDS

#You can run these commands, to check for old MoveRequest or all mailbox in Office365, just remove the hash.
#get-moverequest
#get-mailbox

get-mailbox -id MAILBOX | New-MoveRequest -OutBound -RemoteTargetDatabase DB01 -RemoteHostName owa.domain.prefix -RemoteCredential $ONPREMCREDS -TargetDeliveryDomain ‘InternalAdDomain.prefix’
#Replace MAILBOX with the mailbox
# Replace owa.domain.prefix with the public owa address
# Replace InternalAdDomain.prefix with the internal ad domain name

#You can then run the following commands to check the mailbox moverequest status
Get-MoveRequest | Get-MoveRequestStatistics

Be aware that the user need to be an locally AD users, before you can migrated it back. It can’t be a Cloud users.
See here how to connect the user to an AD user: https://www.codetwo.com/admins-blog/how-to-merge-an-office-365-account-with-an-on-premises-ad-account-after-hybrid-configuration/

Exchange and TLS problems

Today i have a problem that an exchange will not send mails though TLS, no matter what i do.

I find out that, somehow this exchange servers where creating new send connector, with forcehelo = True.

Witch mean that the Exchange server is using the OLD HELO instead of the EHLO, when talking to other SMTP servers. Setting this to false help. Now the Exchange server send with TLS.

So FORCEHELO=$TRUE breaks TLS.

To see if this is the problem, you can type:

get-sendconnector | select id, forcehelo

To set it to false, for the send connector “Default_OUT” you can run this powershell command:

get-sendconnector -id Default_OUT | set-sendconnector -forcehelo $false

Also you might need to set the TLS certificate to the right certificate. Remember that the certificate need to have the hostname in the certificate. Se more on this link:

https://practical365.com/exchange-server/configuring-the-tls-certificate-name-for-exchange-server-receive-connectors/

Error when trying to remove a Mailbox Database on Exchange Server

When you try to delete a mailbox, you may get this error:

This mailbox database contains one or more mailboxes, mailbox plans, archive mailboxes, public folder mailboxes or
arbitration mailboxes. To get a list of all mailboxes in this database, run the command Get-Mailbox -Database
Database ID. To get a list of all mailbox plans in this database, run the command Get-MailboxPlan. To get a list of
archive mailboxes in this database, run the command Get-Mailbox -Database Database ID -Archive. To get a list of all
public folder mailboxes in this database, run the command Get-Mailbox -Database Database ID -PublicFolder. To get a
list of all arbitration mailboxes in this database, run the command Get-Mailbox -Database Database ID -Arbitration.
To disable a non-arbitration mailbox so that you can delete the mailbox database, run the command Disable-Mailbox
Mailbox ID. To disable an archive mailbox so you can delete the mailbox database, run the command Disable-Mailbox
Mailbox ID -Archive. To disable a public folder mailbox so that you can delete the mailbox database, run the command
Disable-Mailbox Mailbox ID -PublicFolder. Arbitration mailboxes should be moved to another server; to do this, run
the command New-MoveRequest Parameters. If this is the last server in the organization, run the command
Disable-Mailbox Mailbox ID -Arbitration -DisableLastArbitrationMailboxAllowed to disable the arbitration mailbox.
Mailbox plans should be moved to another server; to do this, run the command Set-MailboxPlan MailboxPlan ID
-Database Database ID.
    + CategoryInfo          : InvalidOperation: (Database ID:DatabaseIdParameter) [Remove-MailboxDatabase], AssociatedUserMailboxExistException
    + FullyQualifiedErrorId : [Server=Server,RequestId=RequestId,TimeStamp=TimeStamp] [FailureCategory=Cmdlet-AssociatedUserMailboxExistException] XXXXXXXX,Microsoft.Exchange.Management.SystemConfigurationTasks.RemoveMailboxDatabase
    + PSComputerName        : Computer Name

This is because the mailbox database still contains data, that need to be moved, before you can deleted it.
If you run Exchange 2013, run these commands to check witch data, that are still located in the database.

Get-Mailbox -Database "Database" 
Get-Mailbox -Database "Database" -Archive
Get-Mailbox -Database "Database" -Arbitration
Get-Mailbox -Database "Database" -PublicFolder

In Exchange 2016, a new mailbox type is introduce call Auditlog. So here you also need to run this command

Get-Mailbox -Database "Database" -AuditLog

Problems deleting a old mailbox database

I have a customer, where we have taken over the support from an old supplier, so we don’t know much about their background.

The first thing we do, was to update the Exchange Server.
But after have move all mailboxes, public folders, system mailbox.

I still got this error:

This mailbox database contains one or more mailboxes, mailbox plans, archive mailboxes, or arbitration mailboxes. To ge
t a list of all mailboxes in this database, run the command Get-Mailbox -Database <Database ID>. To get a list of all m
ailbox plans in this database, run the command Get-MailboxPlan. To get a list of archive mailboxes in this database, ru
n the command Get-Mailbox -Database <Database ID> -Archive. To get a list of all arbitration mailboxes in this database
, run the command Get-Mailbox -Database <Database ID> -Arbitration. To disable a non-arbitration mailbox so that you ca
n delete the mailbox database, run the command Disable-Mailbox <Mailbox ID>. To disable an archive mailbox so you can d
elete the mailbox database, run the command Disable-Mailbox <Mailbox ID> -Archive. Arbitration mailboxes should be move
d to another server; to do this, run the command New-MoveRequest <parameters>. If this is the last server in the organi
zation, run the command Disable-Mailbox <Mailbox ID> -Arbitration -DisableLastArbitrationMailboxAllowed to disable the
arbitration mailbox. Mailbox plans should be moved to another server; to do this, run the command Set-MailboxPlan <Mail
boxPlan ID> -Database <Database ID>.

Mailbox Plan is only used on the cloud.

I have look every places, unto i run this command:

dsquery * domainroot -attr * -limit 0 > result.txt

i then open the result.txt in notepad, and found some reference to the old database

msExchDisabledArchiveDatabaseLink: CN=Exch2013MB_Archive,CN=Databases,CN=Exchange Administrative Group (FYDIBOHF23SPDLT),CN=Administrative Groups,CN=Fibia,CN=Microsoft Exchange,CN=Services,CN=Configuration,DC=XXXX,DC=local

After delete this reference, i could delete the database.

 

 

 

Give access to calendar based on the user is member of the group.

If you need to give auser access to users calendar, based on if there are member of an group, this is the way.
Start Exchange Management Shell

$users = Get-ADGroupMember [groupname] | select -ExpandProperty name
foreach ($user in $users) {
    $Mailbox = Get-Mailbox $user
	#The Calendar, is name "Kalender" in danish
	add-MailboxFolderPermission -identity “$($Mailbox.Name):\kalender” -AccessRights Editor -User [username]
	set-MailboxFolderPermission -identity “$($Mailbox.Name):\kalender” -AccessRights Editor -User [username]

	#English calendar
	add-MailboxFolderPermission -identity “$($Mailbox.Name):\calendar” -AccessRights Editor -User [username]
	set-MailboxFolderPermission -identity “$($Mailbox.Name):\calendar” -AccessRights Editor -User [username]
} 

Exchange: Get a list of all email address

For en liste over alle mail adresse

Get-recipient -resultsize unlimited | select name -expand emailaddresses | select name,smtpaddress

For en liste over alle mail adresse på et given domæne

Get-Recipient | Where{$_.EmailAddresses -match "it-grp.dk"} | select name -expand emailaddresses | select name,smtpaddress | where {$_ -match "kennethdalbjerg.dk"}