NOTE: If you are using v2.1 update to the most recent release

This is an updated script of the original Purge-Logfiles.ps1 published by Brian Reid here.

Some enterprise environments prefer to not have Exchange Server 2013 installed in the default path on the system partition and even relocate IIS log files to a different partition. To support the utilization from a central script repositiory within an enterprise, there was a need to have the fixed paths variables changed.

Update 2014-12-11: *.blg file filter added to remove performance logs located in \V15\Logging\Diagnostics\DailyPerformanceLogs
Update 2014-12-10: Switch to automatic IIS and Exchange detection added. This allows for manual configuration as well.
Update 2014-12-23: Check for elevated execution added, Write to Exchange Admin Audit Log added
Update 2015-02-06: Handling of IIS default location fixed
Update 2015-02-28: Sorting of server names added and Write-Host output changed
Update 2015-03-21: Count Error fixed
Update 2015-04-28: Email report added
Update 2015-07-02: GlobalFunctions implemetation added
Update 2015-07-29: Minor update to write $DaysToKeep to log
Update 2016-06-05: Minor update due to PowerShell hygiene
Update 2016-07-07: SendMail issue fixed (Thanks to denisvm,
Update 2017-04-10: CopyFilesBeforeDelete implemented 
Update 2017-09-01: Log file archiving and archive compressions added
Update 2017-10-10: Issue #6 fixed | Missing Remove-Item cmdlet
Update 2017-10-11: Issue #7 fixed | SendMail switch query
Update 2018-01-31: Issue #9 fixed | IsEdge parameter added
Update 2018-03-15: Minor changes, no fixes
Uüdate 2018-06-03: Issue #12 fiex | Count property issues

Open topic: Support script execution from a non-Exchange 2013 server.

You can find more information about this script at my blog:

This script requires the GlobalFunctions PowerShell module desribed here:

The GlobalFunctions module must be implemented first. When using PowerShell 5.0 the easiest way is: Install-Module GlobalFunctions.

Find the script at GitHub:
Find the most current release here:



Skript bearbeiten|Remove
.\Purge-LogFiles.ps1 -DaysToKeep 7


Use a dedicated PowerShell file to call the Purge-LogFiles.ps1. This approach simplifies troubleshooting a lot.

Example: Run-PurgeLogFiles.ps1


.\Purge-LogFiles.ps1 -DaysToKeep 7 -SendMail -MailFrom -MailTo -MailServer 



  Purge Exchange 2013+ and IIS logfiles across Exchange servers  
  Thomas Stensitzki 
  (Based Based on the original script by Brian Reid, C7 Solutions (c) 
  Version 2.21, 2018-06-03 
  Ideas, comments and suggestions to  
  This script deletes all Exchange and IIS logs older than X days from all Exchange 2013+ servers 
  that are fetched using the Get-ExchangeServer cmdlet. 
  The Exchange log file location is read from the environment variable and used to build an  
  adminstrative UNC path for file deletions. 
  It is assumed that the Exchange setup path is IDENTICAL across all Exchange servers. 
  The IIS log file location is read from the local IIS metabase of the LOCAL server 
  and is used to build an administrative UNC path for IIS log file deletions. 
  It is assumed that the IIS log file location is identical across all Exchange servers. 
  - Windows Server 2008 R2 SP1, Windows Server 2012 or Windows Server 2012 R2   
  - Utilizes the global function library found here: 
  - Exchange 2013+ Management Shell 
  Revision History  
  1.0     Initial community release  
  1.1     Variable fix and optional code added  
  1.2     Auto/Manual configration options added 
  1.3     Check if running in elevated mode added 
  1.4     Handling of IIS default location fixed 
  1.5     Sorting of server names added and Write-Host output changed 
  1.6     Count Error fixed 
  1.7          Email report functionality added 
  1.8     Support for global logging and other functions added 
  1.9     Global functions updated (write to event log) 
  1.91    Write DaysToKeep to log 
  1.92    .Count issue fixed to run on Windows Server 2012 
  1.93    Minor chances to PowerShell hygiene 
  1.94    SendMail issue fixed (Thanks to denisvm, 
  2.0     Script update 
  2.1     Log file archiving and archive compressions added 
  2.11    Issue #6 fixed  
  2.13    Issue #7 fixed 
  2.14    Issue #9 fixed 
  2.2     Minor changes, but no fixes 
  2.21    Issue #12 fixed 
  .PARAMETER DaysToKeep 
  Number of days Exchange and IIS log files should be retained, default is 30 days 
  Switch to use automatic detection of the IIS and Exchange log folder paths 
  Indicates the the script is executed on an Exchange Server holding the EDGE role. Without the switch servers holding the EDGE role are excluded 
  .PARAMETER RepositoryRootPath 
  Absolute path to a repository folder for storing copied log files and compressed archives. Preferably an UNC path. A new subfolder will be created fpr each Exchange server. 
  .PARAMETER ArchiveMode 
  Log file copy and archive mode. Possible values 
  None = All log files will be purged without being copied 
  CopyOnly = Simply copy log files to the RepositoryRootPath 
  CopyAndZip = Copy logfiles and send copied files to compressed archive 
  CopyZipAndDelete = Same as CopyAndZip, bt delete copied log files from RepositoryRootPath 
  .PARAMETER SendMail 
  Switch to send an Html report 
  .PARAMETER MailFrom 
  Email address of report sender 
  Email address of report recipient 
  .PARAMETER MailServer 
  SMTP Server for email report 
  Delete Exchange and IIS log files older than 14 days  
  .\Purge-LogFiles -DaysToKeep 14 
  Delete Exchange and IIS log files older than 7 days with automatic discovery 
  .\Purge-LogFiles -DaysToKeep 7 -Auto 
  Delete Exchange and IIS log files older than 7 days with automatic discovery and send email report 
  .\Purge-LogFiles -DaysToKeep 7 -Auto -SendMail -MailFrom -MailTo -MailServer  
  Delete Exchange and IIS log files older than 14 days, but copy files to a central repository and compress the log files before final deletion 
  .\Purge-LogFiles -DaysToKeep 14 -RepositoryRootPath \\OTHERSERVER\OtherShare\LOGS -ArchiveMode CopyZipAndDelete 
  [int] $DaysToKeep = 30,   
  [string] $MailFrom = '', 
  [string] $MailTo = '', 
  [string] $MailServer = '', 
  [string] $RepositoryRootPath = '\\MYSERVER\SomeShare\EXCHANGELOGS', 
  [ValidateSet('None','CopyOnly','CopyAndZip','CopyZipAndDelete')] #Available archive modes, default: NONE 
  [string] $ArchiveMode = 'None' 
## Set fixed IIS and Exchange log paths  
## Examples:  
##   "C$\inetpub\logs\LogFiles" 
##   "C$\Program Files\Microsoft\Exchange Server\V15\Logging" 
[string]$IisUncLogPath = 'D$\IISLogs' 
[string]$ExchangeUncLogPath = 'E$\Program Files\Microsoft\Exchange Server\V15\Logging' 
# log file extension filter 
[string[]]$IncludeFilter = @('*.log') 
# filename template for archived log files 
[string]$ArchiveFileName =  "LogArchive $(Get-Date -Format 'yyyy-MM-dd HHmm').zip" 
# Folder name for a per Exchange server log files 
# Folder is used as target when copying log files 
# Folder will be deleted when using ArchiveMode CopyZipAndDelete 
[string]$LogSubfolderName = 'LOGS' 
# Some error variables 
$ERR_OK = 0 
# Preset some archive switches 
[bool]$CopyFiles = $false 
[bool]$ZipArchive = $false 
[bool]$DeleteZippedFiles = $false 
# Import Exchange functions 
Add-PSSnapin -Name Microsoft.Exchange.Management.PowerShell.SnapIn 
# 2015-06-18: Implementation of global functions module 
Import-Module -Name GlobalFunctions 
$ScriptDir = Split-Path -Path $script:MyInvocation.MyCommand.Path 
$ScriptName = $MyInvocation.MyCommand.Name 
$logger = New-Logger -ScriptRoot $ScriptDir -ScriptName $ScriptName -LogFileRetention 14 
$logger.Write('Script started') 
if($Auto) { 
    # detect log file locations automatically and set variables 
    [string]$ExchangeInstallPath = $env:ExchangeInstallPath 
    [string]$ExchangeUncLogDrive = $ExchangeInstallPath.Split(':\')[0] 
    $ExchangeUncLogPath = ('{0}$\{1}Logging\' -f ($ExchangeUncLogDrive), $ExchangeInstallPath.Remove(0,3)) 
    # Fetch local IIS log location from Metabase 
    # IIS default location fixed 2015-02-02 
    [string]$IisLogPath = ((Get-WebConfigurationProperty -Filter 'system.applicationHost/sites/siteDefaults' -Name logFile).directory).Replace('%SystemDrive%',$env:SystemDrive) 
    # Extract drive letter and build log path 
    [string]$IisUncLogDrive =$IisLogPath.Split(':\')[0]  
    $IisUncLogPath = $IisUncLogDrive + '$\' + $IisLogPath.Remove(0,3)  
function Copy-LogFiles { 
    [string] $SourceServer = '', 
    [string]$SourcePath = '', 
    [string]$ArchivePrefix = '' 
  if($SourceServer -ne '') {  
    # path per SERVER for zipped archives 
    $ServerRepositoryPath = Join-Path -Path $RepositoryRootPath -ChildPath $SourceServer 
    # subfolder used as target for copying source folders and files 
    $ServerRepositoryLogsPath = Join-Path -Path $ServerRepositoryPath -ChildPath $LogSubfolderName 
    $ServerRepositoryPath = Join-Path -Path $RepositoryRootPath -ChildPath $SourceServer 
    if(!(Test-Path -Path $ServerRepositoryPath)) { 
      # Create new target directory for server, if does not exist 
      $null = New-Item -Path $ServerRepositoryPath -ItemType Directory -Force -Confirm:$false 
    foreach ($File in $FilesToMove) { 
      # target directory 
      $targetDir = $File.DirectoryName.Replace($TargetServerFolder$ServerRepositoryLogsPath) 
      # target file path 
      $targetFile = $File.FullName.Replace($TargetServerFolder$ServerRepositoryLogsPath) 
      # create target directory, if not exists 
      if(!(Test-Path -Path $targetDir)) {$null = mkdir -Path $targetDir} 
      # copy file to target 
      $null = Copy-Item -Path $File.FullName -Destination $targetFile -Recurse -Force -Confirm:$false -ErrorAction SilentlyContinue 
    if($ZipArchive) { 
      # zip copied log files 
      $Archive = Join-Path -Path $ServerRepositoryPath -ChildPath ('{0}-{1}' -$ArchivePrefix$ArchiveFileName) 
      $logger.Write(('Zip copied files to {0}' -$ArchiveFileName)) 
      # delete archive file, if already exists 
      if(Test-Path -Path $Archive) {Remove-Item -Path $Archive -Force -Confirm:$false} 
      try { 
        # create zipped asrchive 
        # Add-Type -AssemblyName 'System.IO.Compression.FileSystem' 
      catch { 
        $ErrorMessage = $_.Exception.Message 
        $logger.Write(('Error compressing files from {0} to {1}. Error Message: {2}' -$ServerRepositoryLogsPath$Archive$ErrorMessage),3)       
      finally { 
        # cleanup, if compression was successful 
        if($DeleteZippedFiles) { 
          $logger.Write(('Deleting folder {0}' -$ServerRepositoryLogsPath)) 
          $null = Remove-Item -Path $ServerRepositoryLogsPath -Recurse -Force -Confirm:$false -ErrorAction SilentlyContinue 
# Function to clean log files from remote servers using UNC paths 
function Remove-LogFiles { 
    [Parameter(Mandatory, HelpMessage='Absolute path to log file source')] 
    [string]$Type = 'IIS' 
  # Build full UNC path 
  $TargetServerFolder = ('\\{0}\{1}' -f ($E15Server), ($Path)) 
  # Write progress bar for current activity 
  Write-Progress -Activity ('Checking files on Server {0}' -$E15Server-Status ('Checking files in {0}' -$TargetServerFolder-PercentComplete(($i/$max)*100) 
  # Only try to delete files, if folder exists 
  if (Test-Path -Path $TargetServerFolder) { 
      $LastWrite = (Get-Date).AddDays(-$DaysToKeep) 
      # Select files to delete 
      $Files = Get-ChildItem -Path $TargetServerFolder -Include $IncludeFilter -Recurse -ErrorAction SilentlyContinue | Where-Object {$_.LastWriteTime -le $LastWrite} 
      $FilesToDelete = ($Files | Measure-Object).Count 
      # Lets count the files that will be deleted 
      $fileCount = 0 
      if($FilesToDelete -gt 0) { 
        if($CopyFiles) { 
          # we want to copy all files to central repository before final deletion 
          $logger.Write(('Copy {0} files from {1} to repository' -$FilesToDelete.Count, $TargetServerFolder)) 
          # Write progress bar for current activity 
          Write-Progress -Activity ('Checking files on Server {0}' -$E15Server-Status 'Copying log files' -PercentComplete(($i/$max)*100) 
          Copy-LogFiles -SourceServer $E15Server -SourcePath $TargetServerFolder -FilesToMove $Files -ArchivePrefix $Type 
        # Delete the files -> purge 
        foreach ($File in $Files) { 
          $null = Remove-Item -Path $File -ErrorAction SilentlyContinue -Force  
        $logger.Write(('{0} files deleted in {1}' -$fileCount$TargetServerFolder)) 
        #Html output 
        $Output = ("<li>{0} files deleted in '{1}'</li>" -$fileCount$TargetServerFolder) 
      else { 
        $logger.Write(('No files to delete in {0}' -$TargetServerFolder)) 
        #Html output 
        $Output = ("<li>No files to delete in '{0}'</li>" -$TargetServerFolder) 
  Else { 
      # oops, folder does not exist or is not accessible 
      Write-Host ("The folder {0} doesn't exist or is not accessible! Check the folder path!" -$TargetServerFolder-ForegroundColor Red 
      #Html output 
      $Output = ("The folder {0} doesn't exist or is not accessible! Check the folder path!" -$TargetServerFolder) 
# Check if we are running in elevated mode 
# function (c) by Michel de Rooij, 
Function script:Test-IsAdmin { 
    $currentPrincipal = New-Object -TypeName Security.Principal.WindowsPrincipal -ArgumentList ( [Security.Principal.WindowsIdentity]::GetCurrent() ) 
    If$currentPrincipal.IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator )) { 
        return $true 
    else { 
        return $false 
# Check validity of parameters required for sending emails  
Function script:Test-SendMail { 
     if( ($MailFrom -ne ''-and ($MailTo -ne ''-and ($MailServer -ne '') ) { 
        return $true 
     else { 
        return $false 
# Main ----------------------------------------------------- 
If ($SendMail.IsPresent) {  
  If (-Not (Test-SendMail)) { 
      Throw 'If -SendMail specified, -MailFrom, -MailTo and -MailServer must be specified as well!' 
Switch($ArchiveMode) { 
  'CopyOnly' { 
    $CopyFiles = $true 
  'CopyAndZip' { 
    $CopyFiles = $true 
    $ZipArchive = $true 
  'CopyZipAndDelete' { 
    $CopyFiles = $true 
    $ZipArchive = $true 
    $DeleteZippedFiles = $true 
  default { } 
If (Test-IsAdmin) { 
    # We are running in elevated mode. Let's continue. 
    Write-Output ('Removing IIS and Exchange logs - Keeping last {0} days - Be patient, it might take some time' -$DaysToKeep) 
    # Track script execution in Exchange Admin Audit Log  
    Write-AdminAuditLog -Comment 'Purge-LogFiles started!' 
    $logger.Write(('Purge-LogFiles started, keeping last {0} days of log files.' -f ($DaysToKeep))) 
    $AllExchangeServers = $null 
    if($IsEdge) {  
      # Get a list of all Exchange V15* servers 
      $AllExchangeServers = Get-ExchangeServer | Where-Object {$_.IsE15OrLater -eq $true| Sort-Object -Property Name 
    else { 
      # Get a list of all Exchange V15* servers, exclude server service the EDGE role 
      $AllExchangeServers = Get-ExchangeServer | Where-Object {($_.IsE15OrLater -eq $true-and ($_.ServerRole -notmatch 'Edge')} | Sort-Object -Property Name 
    $logger.WriteEventLog(('Script started. Script will purge log files on: {0}' -$AllExchangeServers)) 
    # Lets count the steps for a nice progress bar 
    $i = 1 
    # Issue #12, Using Mesaure-Object to ensure that we have a Count property 
    $max = ($AllExchangeServers | Measure-Object).Count * 2 # two actions to execute per server 
    # Prepare Output 
    $Output = '<html> 
    <font size=""1"" face=""Arial,sans-serif"">' 
    # Call function for each server and each directory type 
    foreach ($E15Server In $AllExchangeServers) { 
        # Write-Host "Working on: $E15Server" -ForegroundColor Gray 
        $Output += ('<h5>{0}</h5> 
        <ul>' -$E15Server) 
        # Remove IIS log files 
        $Output += Remove-LogFiles -Path $IisUncLogPath -Type IIS 
        # Remove Exchange files 
        $Output += Remove-LogFiles -Path $ExchangeUncLogPath -Type Exchange 
    # Finalize Output 
    if($SendMail) { 
        $logger.Write(('Sending email to {0}' -$MailTo)) 
        try { 
            Send-Mail -From $MailFrom -To $MailTo -SmtpServer $MailServer -MessageBody $Output -Subject 'Purge-Logfiles Report'          
        catch { 
            $logger.Write(('Error sending email to {0}' -$MailTo),3) 
    $logger.Write('Script finished') 
    Return $ERR_OK 
else { 
    # Ooops, the admin did it again. 
    Write-Output 'The script need to be executed in elevated mode. Start the Exchange Management Shell with administrative privileges.'