Script to Create a Summary Overview and Full Report of all Contact Objects in a Domain

by Jeremy Saunders on January 2, 2015

This PowerShell script is one of the most comprehensive you will find that provides a thorough overview and full report of all contact objects in a domain. It is the culmination of many Active Directory audit and reviews and therefore contains valuable input from many customers.

A lot of thought has been put into the logic within this script to help an organisation understand:

  • Contacts that are mail-disabled
  • Contacts that are ADFS Farm objects, which are Contact objects located under the certificate sharing container.
  • Contacts that are UM Integration objects
  • Contacts that are conflicting/duplicate objects (name contains CNF:)
  • Contacts that have expired
  • Contacts that have no manager set
  • Contacts that have been left in the default Users container (CN=Users)

FYI:

  • Mail-enabled contacts are derived from the targetAddress, proxyAddresses, legacyExchangeDN, and mailNickName attributes.

Open the full CSV report in Excel and add a column filter and freeze the top row. This will help you to filter the data and move through the spreadsheet with ease.

Contacts are typically used to represent external users for the purpose of e-mail. But before making this statement you need to ensure that either a 3rd party or in house application are not making use of them in other ways.

Contacts can be ‘disabled’ by mail-disabling them. Always clearly document situations where contacts have been disabled. i.e. Are they being kept for a reason, such as for their phone details, etc, or should they be deleted. Delete them if they are no longer valid.

Contacts should not be left in the default Users container (ie. CN=Users). Move them to a more appropriate and meaningful location.

Contacts whose name contains CNF: means that it’s a duplicate object caused by conflicting/duplicate objects. This typically occurs when objects are created on different Read Write Domain Controllers at nearly the same time. After replication kicks in and those conflicting/duplicate objects replicate to other Read Write Domain Controllers, Active Directory replication applies a conflict resolution mechanism to ensure every object is and remains unique. Verify that there is a matching normal object, merge attributes and then the conflicting/duplicate object can be deleted.

ADFS Farm objects under the certificate sharing container need to be managed by the ADFS team and not by this process. They are not a traditional contact object. I always found it bizarre that Microsoft chose to create these as Contact objects. I don’t have an issue with it, but just find it dangerous to think that some IT Pros may just blindly run a script to delete all old contact objects before understanding what they really are!

From here you can start to implement some good policies and processes around the management of contacts.

At the end of the day a nice way to manage contacts is to set their expirationTime attribute. This will give us the ability to implement a nice life cycle management process. You can go one step further and add a user or mail-enabled security group to the manager attribute. This will give us the ability to implement some workflow when the contact is x days before expiring.

The following screen shot is from a recent health check and audit I completed.

There are 4 things here that stick out here like a sore thumb:

  • 9.35% of Contacts are mail-disabled. Are they serving any purpose?
  • None of the Contacts have an expiration time set
  • More or less none of the Contacts have a manager set
  • Contacts left in the default Users container

Contact Object Overview

It’s very clear to me just by looking at this that they have no life cycle management process in place for their Contact objects.

The following screen shot is the full report from the same health check. Whilst I’ve had to blur our some of the data you can get an idea from the column headings that it’s fairly extensive. There are a further 8 columns that I was unable to capture in the screen shot due to screen resolution.

Contact Object Full Report

Here is the Get-ContactReport.ps1 (844 downloads)  script:

<# This script will enumerate all contact objects in a Domain, providing both a summary overview and a full report based on the values of the following attributes: - name - distinguishedname - givenName - sn - initials - mail - telephoneNumber - mobile - displayName - description - title - company - physicalDeliveryOfficeName - manager - employeeID - employeeNumber - employeeType - msexchextensioncustomattribute1 - msexchextensioncustomattribute2 - msexchextensioncustomattribute3 - msexchextensioncustomattribute4 - msexchextensioncustomattribute5 - isCriticalSystemObject - expirationtime - info - whencreated - whenchanged - memberOf - targetAddress - proxyAddresses - legacyExchangeDN - mailNickName - msRTCSIP-Line - otherIpPhone We further derive: - The Parent OU from the distinguishedname attribute. - MailEnabled from targetAddress, proxyAddresses, legacyExchangeDN, and mailNickName. - http://pmoreland.blogspot.com.au/2013/03/creating-mail-contacts-and-distribution.html - UMIntegrationObject from msRTCSIP-Line and otherIpPhone We check to see if the Surname (sn) contains a non-alpha character. This is to ensure that a consistent standard/pattern has been used for hyphenated namnes, some Asian names, and tussenvoegsels. Syntax examples: - To execute the script in the current Domain: Get-ContactReport.ps1 - To execute the script against a trusted Domain: Get-ContactReport.ps1 -TrustedDomain mydemosthatrock.com Script Name: Get-ContactReport.ps1 Release 2.3 Written by Jeremy@jhouseconsulting.com 27/12/2013 Modified by Jeremy@jhouseconsulting.com 14/12/2015 #>
#-------------------------------------------------------------
param([String]$TrustedDomain,[switch]$verbose)

if ($verbose.IsPresent) { 
  $VerbosePreference = 'Continue' 
  Write-Verbose "Verbose Mode Enabled" 
} 
Else { 
  $VerbosePreference = 'SilentlyContinue' 
}

#-------------------------------------------------------------

# Set this to the OU structure where the you want to search to
# start from. Do not add the Domain DN. If you leave it blank,
# the script will start from the root of the domain.
$OUStructureToProcess = ""

# Set this to true to include the employeeID, employeeNumber and
# employeeType attributes.
$IncludeEmployeeAttributes = $True

# Set this to true to include the first 3 msexchextensioncustomattribute
# attributes. Really handy for Office365 license management with DirSync.
$IncludeExchExtensionCustomAttributes = $False

# Set this to true to include extra contact attributes such as
# displayName, description, telephoneNumber, mobile, title,
# company, physicalDeliveryOfficeName
$IncludeExtendedDetails = $True

# Set this to true to include the user's direct group membership
$IncludeMemberOf = $True

# Set the name of the attribute you want to populate for objects
# to be evaluated as a stale or non-stale object.
$ExcludeAttribute = "comment"

# Set the text within the $ExcludeAttribute that you want to use
# to evaluate if the object should be excluded from the stale
# object collection.
$ExcludeText = "Decommission=False"

# Set this to the delimiter for the CSV output
$Delimiter = ","

# Set this to remove the double quotes from each value within the
# CSV.
$RemoveQuotesFromCSV = $False

# Set this value to true if you want to see the progress bar.
$ProgressBar = $True

#-------------------------------------------------------------

$invalidChars = [io.path]::GetInvalidFileNamechars() 
$datestampforfilename = ((Get-Date -format s).ToString() -replace "[$invalidChars]","-")

# Get the script path
$ScriptPath = {Split-Path $MyInvocation.ScriptName}
$ReferenceFileFull = $(&$ScriptPath) + "\ContactReport-$($datestampforfilename).csv"
$ReferenceFileSummaryTotals = $(&$ScriptPath) + "\ContactReport-Summary-Totals-$($datestampforfilename).csv"

if (Test-Path -path $ReferenceFileFull) {
  remove-item $ReferenceFileFull -force -confirm:$false
}
if (Test-Path -path $ReferenceFileSummaryTotals) {
  remove-item $ReferenceFileSummaryTotals -force -confirm:$false
}

if ([String]::IsNullOrEmpty($TrustedDomain)) {
  # Get the Current Domain Information
  $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
} else {
  $context = new-object System.DirectoryServices.ActiveDirectory.DirectoryContext("domain",$TrustedDomain)
  Try {
    $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($context)
  }
  Catch [exception] {
    write-host -ForegroundColor red $_.Exception.Message
    Exit
  }
}

# Get AD Distinguished Name
$DomainDistinguishedName = $Domain.GetDirectoryEntry() | select -ExpandProperty DistinguishedName  

If ($OUStructureToProcess -eq "") {
  $ADSearchBase = $DomainDistinguishedName
} else {
  $ADSearchBase = $OUStructureToProcess + "," + $DomainDistinguishedName
}

$TotalcontactsProcessed = 0
$contactCount = 0
$TotalMailEnabledObjects = 0
$TotalMailDisabledObjects = 0
$TotalDefaultUsersContainer = 0
$TotalUMIntegrationObjects = 0
$TotalADFSFarmObjects = 0
$TotalConflictingObjects = 0
$TotalManagerNotSet = 0
$TotalExpirationTimeNotSet = 0
$TotalExcludedObjects = 0

# Create an LDAP search for all contacts
$ADFilter = "(&(objectClass=contact)(objectcategory=person))"

# There is a known bug in PowerShell requiring the DirectorySearcher
# properties to be in lower case for reliability.
$ADPropertyList = @("name","distinguishedname","givenname","sn","initials","mail", `
                    "telephonenumber","mobile","displayname","description","title", `
                    "company","physicaldeliveryofficename","manager","employeeid", `
                    "employeenumber","employeetype","info","iscriticalsystemobject", `
                    "whencreated","whenchanged","expirationtime","memberof", `
                    $ExcludeAttribute,"msexchextensioncustomattribute1", `
                    "msexchextensioncustomattribute2","msexchextensioncustomattribute3", `
                    "msexchextensioncustomattribute4","msexchextensioncustomattribute5", `
                    "targetaddress","proxyaddresses","legacyexchangedn","mailnickname", `
                    "msrtcsip-line","otheripphone")

$ADScope = "SUBTREE"
$ADPageSize = 1000
$ADSearchRoot = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$($ADSearchBase)") 
$ADSearcher = New-Object System.DirectoryServices.DirectorySearcher
$ADSearcher.SearchRoot = $ADSearchRoot
$ADSearcher.PageSize = $ADPageSize 
$ADSearcher.Filter = $ADFilter 
$ADSearcher.SearchScope = $ADScope
if ($ADPropertyList) {
  foreach ($ADProperty in $ADPropertyList) {
    [Void]$ADSearcher.PropertiesToLoad.Add($ADProperty)
  }
}
Try {
  write-host -ForegroundColor Green "`nPlease be patient whilst the script retrieves all contact objects and specified attributes..."
  $colResults = $ADSearcher.Findall()
  # Dispose of the search and results properly to avoid a memory leak
  $ADSearcher.Dispose()
  $contactCount = $colResults.Count
}
Catch {
  $contactCount = 0
  Write-Host -ForegroundColor red "The $ADSearchBase structure cannot be found!"
}

if ($contactCount -ne 0) {
  write-host -ForegroundColor Green "`nProcessing $contactCount contact objects in the $domain Domain..."
  $colResults | ForEach-Object {
    $contactDN = $_.Properties.distinguishedname[0]
    $ParentOU = $contactDN -split '(?<![\\]),'
    $ParentOU = $ParentOU[1..$($ParentOU.Count-1)] -join ','
    $Name = $_.Properties.name[0]
    If (($_.Properties.givenname | Measure-Object).Count -gt 0) {
      $Firstname = $_.Properties.givenname[0]
    } else {
      $Firstname = ""
    }
    If (($_.Properties.sn | Measure-Object).Count -gt 0) {
      $Surname = $_.Properties.sn[0]
      $nonalphacharsinsurname = $Surname -match '[^a-zA-Z]'
    } else {
      $Surname = ""
      $nonalphacharsinsurname = $False
    }
    If (($_.Properties.initials | Measure-Object).Count -gt 0) {
      $Initials = $_.Properties.initials[0]
    } else {
      $Initials = ""
    }
    If (($_.Properties.employeeid | Measure-Object).Count -gt 0) {
      $EmployeeID = $_.Properties.employeeid[0]
    } else {
      $EmployeeID = ""
    }
    If (($_.Properties.employeenumber | Measure-Object).Count -gt 0) {
      $EmployeeNumber = $_.Properties.employeenumber[0]
    } else {
      $EmployeeNumber = ""
    }
    If (($_.Properties.employeetype | Measure-Object).Count -gt 0) {
      $EmployeeType = $_.Properties.employeetype[0]
    } else {
      $EmployeeType = ""
    }
    If (($_.Properties.mail | Measure-Object).Count -gt 0) {
      $EMail = $_.Properties.mail[0]
    } else {
      $EMail = ""
    }

    $MailEnabled = $False
    If (($_.Properties.targetaddress | Measure-Object).Count -gt 0 -AND
        ($_.Properties.proxyaddresses | Measure-Object).Count -gt 0 -AND
        ($_.Properties.legacyexchangedn | Measure-Object).Count -gt 0 -AND
        ($_.Properties.mailnickname | Measure-Object).Count -gt 0 ) {
      $MailEnabled = $True
      $TotalMailEnabledObjects = $TotalMailEnabledObjects + 1
    } Else {
      $TotalMailDisabledObjects = $TotalMailDisabledObjects + 1
    }

    If ($IncludeExtendedDetails) {
      If (($_.Properties.displayname | Measure-Object).Count -gt 0) {
        $DisplayName = $_.Properties.displayname[0]
      } else {
        $DisplayName = ""
      }
      If (($_.Properties.description | Measure-Object).Count -gt 0) {
        $Description = $_.Properties.description[0]
      } else {
        $Description = ""
      }
      If (($_.Properties.telephonenumber | Measure-Object).Count -gt 0) {
        $TelephoneNumber = $_.Properties.telephonenumber[0].Replace(";","|")
      } else {
        $TelephoneNumber = ""
      }
      If (($_.Properties.mobile | Measure-Object).Count -gt 0) {
        $Mobile = $_.Properties.mobile[0].Replace(";","|")
      } else {
        $Mobile = ""
      }
      If (($_.Properties.title | Measure-Object).Count -gt 0) {
        $Title = $_.Properties.title[0]
      } else {
        $Title = ""
      }
      If (($_.Properties.company | Measure-Object).Count -gt 0) {
        $Company = $_.Properties.company[0]
      } else {
        $Company = ""
      }
      If (($_.Properties.physicaldeliveryofficename | Measure-Object).Count -gt 0) {
        $Office = $_.Properties.physicaldeliveryofficename[0]
      } else {
        $Office = ""
      }
    }

    If (($_.Properties.manager | Measure-Object).Count -gt 0) {
      $Manager = $_.Properties.manager[0]
    } else {
      $Manager = ""
      $TotalManagerNotSet = $TotalManagerNotSet + 1
    }

    If (($_.Properties.msexchextensioncustomattribute1 | Measure-Object).Count -gt 0) {
      $msExchExtensionCustomAttribute1 = $_.Properties.msexchextensioncustomattribute1[0]
    } else {
      $msExchExtensionCustomAttribute1 = ""
    }
    If (($_.Properties.msexchextensioncustomattribute2 | Measure-Object).Count -gt 0) {
      $msExchExtensionCustomAttribute2 = $_.Properties.msexchextensioncustomattribute2[0]
    } else {
      $msExchExtensionCustomAttribute2 = ""
    }
    If (($_.Properties.msexchextensioncustomattribute3 | Measure-Object).Count -gt 0) {
      $msExchExtensionCustomAttribute3 = $_.Properties.msexchextensioncustomattribute3[0]
    } else {
      $msExchExtensionCustomAttribute3 = ""
    }
    If (($_.Properties.msexchextensioncustomattribute4 | Measure-Object).Count -gt 0) {
      $msExchExtensionCustomAttribute4 = $_.Properties.msexchextensioncustomattribute4[0]
    } else {
      $msExchExtensionCustomAttribute4 = ""
    }
    If (($_.Properties.msexchextensioncustomattribute5 | Measure-Object).Count -gt 0) {
      $msExchExtensionCustomAttribute5 = $_.Properties.msexchextensioncustomattribute5[0]
    } else {
      $msExchExtensionCustomAttribute5 = ""
    }

    $whencreated = $_.Properties.whencreated[0]
 
    # If it was created less than 30 days ago mark it as a new account.
    If ($whencreated -gt (Get-Date).AddDays(-30)) {
      $IsNew = $True
    } else {
      $IsNew = $False
    }

    $whenchanged = $_.Properties.whenchanged[0]

    If (($_.Properties.expirationtime | Measure-Object).Count -gt 0) {
      $ExpirationTime = $_.Properties.expirationtime
    } Else {
      $ExpirationTime = $NULL
      $TotalExpirationTimeNotSet = $TotalExpirationTimeNotSet + 1
    }
    If ($ExpirationTime -ne $NULL -AND $ExpirationTime -lt (Get-Date)) {
      $Expired = $True
    } Else {
      $Expired = $False
    }

    # Check if it should be excluded
    $Exclude = $False
    If (($_.Properties.$ExcludeAttribute | Measure-Object).Count -gt 0) {
      If ($_.Properties.$ExcludeAttribute[0] -eq $ExcludeText) {
        $Exclude = $True
        $TotalExcludedObjects = $TotalExcludedObjects + 1
      }
    }

    $Conflict = $False
    If ($Name -Like "*CNF:*") {
      # Replace the Line Feed character in the name so that it's a nicely represented string.
      $Name = $Name -replace "`n",""
      $Conflict = $True
      $TotalConflictingObjects = $TotalConflictingObjects + 1
    }

    If ($ParentOU -Like "CN=Users*") {
      $TotalDefaultUsersContainer = $TotalDefaultUsersContainer + 1
    }

    If (($_.Properties.iscriticalsystemobject | Measure-Object).Count -gt 0) {
      $isCriticalSystemObject = $_.Properties.iscriticalsystemobject[0]
    } else {
      $isCriticalSystemObject = ""
    }

    $UMIntegrationObject = $False
    If (($_.Properties.'msrtcsip-line' | Measure-Object).Count -gt 0 -AND
        ($_.Properties.otheripphone | Measure-Object).Count -gt 0 ) {
      If ($_.Properties.'msrtcsip-line' -like "tel:*" -AND 
          $_.Properties.otheripphone -like "sip:*" -AND
          $_.Properties.otheripphone -like "*;opaque=app:exum:*" ) {
        $UMIntegrationObject = $True
        $TotalUMIntegrationObjects = $TotalUMIntegrationObjects + 1
      }
    }

    $ADFSObject = $False
    If ($ParentOU -Like "*CN=ADFS,CN=Microsoft,CN=Program Data*") {
      $ADFSObject = $True
      $TotalADFSFarmObjects = $TotalADFSFarmObjects + 1
    }

    If (($_.Properties.info | Measure-Object).Count -gt 0) {
      $notes = $_.Properties.info[0]
      $notes = $notes -replace "`r`n", "|"
    } else {
      $notes = ""
    }

    If ($IncludeMemberOf) {
      $Members = ""
      $groups = $_.Properties.memberof | ForEach {
        $groupDN = $_
        $objGroup = [ADSI]("LDAP://" + $groupDN)
        $Member = $objGroup.samaccountname
          If ($Members -ne "" ) {
            $Members += "|" + $Member
          } else {
            $Members += $Member
          }
          $objGroup = $null
        }
    }

    $obj = New-Object -TypeName PSObject
    $obj | Add-Member -MemberType NoteProperty -Name "Name" -value $Name
    $obj | Add-Member -MemberType NoteProperty -Name "ParentOU" -value $ParentOU
    $obj | Add-Member -MemberType NoteProperty -Name "Firstname" -value $Firstname
    $obj | Add-Member -MemberType NoteProperty -Name "Surname" -value $Surname
    $obj | Add-Member -MemberType NoteProperty -Name "NonAlphaCharsInSurname" -value $nonalphacharsinsurname
    $obj | Add-Member -MemberType NoteProperty -Name "Initials" -value $Initials
    $obj | Add-Member -MemberType NoteProperty -Name "E-Mail" -value $EMail
    $obj | Add-Member -MemberType NoteProperty -Name "MailEnabled" -value $MailEnabled
    If ($IncludeEmployeeAttributes) {
      $obj | Add-Member -MemberType NoteProperty -Name "EmployeeID" -value $EmployeeID
      $obj | Add-Member -MemberType NoteProperty -Name "EmployeeNumber" -value $EmployeeNumber
      $obj | Add-Member -MemberType NoteProperty -Name "EmployeeType" -value $EmployeeType
    }
    If ($IncludeExtendedDetails) {
      $obj | Add-Member -MemberType NoteProperty -Name "DisplayName" -value $DisplayName
      $obj | Add-Member -MemberType NoteProperty -Name "Description" -value $Description
      $obj | Add-Member -MemberType NoteProperty -Name "TelephoneNumber" -value $TelephoneNumber
      $obj | Add-Member -MemberType NoteProperty -Name "Mobile" -value $Mobile
      $obj | Add-Member -MemberType NoteProperty -Name "Title" -value $Title
      $obj | Add-Member -MemberType NoteProperty -Name "Company" -value $Company
      $obj | Add-Member -MemberType NoteProperty -Name "Office" -value $Office
    }
    $obj | Add-Member -MemberType NoteProperty -Name "Manager" -value $Manager
    If ($IncludeExchExtensionCustomAttributes) {
      $obj | Add-Member -MemberType NoteProperty -Name "msExchExtensionCustomAttribute1" -value $msExchExtensionCustomAttribute1
      $obj | Add-Member -MemberType NoteProperty -Name "msExchExtensionCustomAttribute2" -value $msExchExtensionCustomAttribute2
      $obj | Add-Member -MemberType NoteProperty -Name "msExchExtensionCustomAttribute3" -value $msExchExtensionCustomAttribute3
      $obj | Add-Member -MemberType NoteProperty -Name "msExchExtensionCustomAttribute4" -value $msExchExtensionCustomAttribute4
      $obj | Add-Member -MemberType NoteProperty -Name "msExchExtensionCustomAttribute5" -value $msExchExtensionCustomAttribute5
    }
    $obj | Add-Member -MemberType NoteProperty -Name "Exclude" -value $Exclude
    $obj | Add-Member -MemberType NoteProperty -Name "IsNew" -value $IsNew
    $obj | Add-Member -MemberType NoteProperty -Name "ExpirationTime" -value $ExpirationTime
    $obj | Add-Member -MemberType NoteProperty -Name "Expired" -value $Expired
    $obj | Add-Member -MemberType NoteProperty -Name "WhenCreated" -value $whencreated
    $obj | Add-Member -MemberType NoteProperty -Name "WhenChanged" -value $whenchanged
    $obj | Add-Member -MemberType NoteProperty -Name "Conflicting" -value $Conflict
    $obj | Add-Member -MemberType NoteProperty -Name "isCriticalSystemObject" -value $isCriticalSystemObject
    $obj | Add-Member -MemberType NoteProperty -Name "UMIntegrationObject" -value $UMIntegrationObject
    $obj | Add-Member -MemberType NoteProperty -Name "ADFSObject" -value $ADFSObject
    $obj | Add-Member -MemberType NoteProperty -Name "Notes" -value $Notes
    If ($IncludeMemberOf) {
      $obj | Add-Member -MemberType NoteProperty -Name "MemberOf" -value $Members
    }

    # PowerShell V2 doesn't have an Append parameter for the Export-Csv cmdlet. Out-File does, but it's
    # very difficult to get the formatting right, especially if you want to use quotes around each item
    # and add a delimeter. However, we can easily do this by piping the object using the ConvertTo-Csv,
    # Select-Object and Out-File cmdlets instead.
    if ($PSVersionTable.PSVersion.Major -gt 2) {
      $obj | Export-Csv -Path "$ReferenceFileFull" -Append -Delimiter $Delimiter -NoTypeInformation -Encoding ASCII
    } Else {
      if (!(Test-Path -path $ReferenceFileFull)) {
        $obj | ConvertTo-Csv -NoTypeInformation -Delimiter $Delimiter | Select-Object -First 1 | Out-File -Encoding ascii -filepath "$ReferenceFileFull"
      }
      $obj | ConvertTo-Csv -NoTypeInformation -Delimiter $Delimiter | Select-Object -Skip 1 | Out-File -Encoding ascii -filepath "$ReferenceFileFull" -append -noclobber
    }

    $TotalcontactsProcessed ++
    If ($ProgressBar) {
      Write-Progress -Activity 'Processing Contacts' -Status ("Count: $($TotalcontactsProcessed) - Name: {0}" -f $Name) -PercentComplete (($TotalcontactsProcessed/$contactCount)*100)
    }

  }

  # Dispose of the search and results properly to avoid a memory leak
  $colResults.Dispose()

  # Remove the quotes from the output file.
  If ($RemoveQuotesFromCSV) {
    (get-content "$ReferenceFileFull") |% {$_ -replace '"',""} | out-file "$ReferenceFileFull" -Fo -En ascii
  }

  # Note that for the summary output I went with a hashtable instead of binding multiple objects together.
  # Whilst some of the code may seem excessive and repetitive, I found this the simplest method to achieve
  # the desired output.
  $SummaryHashTable = @{}
  $Item = 0

  $Summaryobj = New-Object -TypeName PSObject
  $percent = "{0:P}" -f ($TotalMailEnabledObjects/$ContactCount)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Item" -value ($Item = $Item + 1)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Statement" -value "Contacts that are mail-enabled"
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Total_Count" -value $TotalMailEnabledObjects
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Overall_Percentage" -value $percent
  $SummaryHashTable = $SummaryHashTable + @{"Contacts that are mail-enabled" = $Summaryobj}
  $Summaryobj = $Null

  $Summaryobj = New-Object -TypeName PSObject
  $percent = "{0:P}" -f (($TotalMailDisabledObjects - $TotalADFSFarmObjects)/$ContactCount)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Item" -value ($Item = $Item + 1)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Statement" -value "Contacts that are mail-disabled (excluding ADFS Farm objects)"
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Total_Count" -value ($TotalMailDisabledObjects - $TotalADFSFarmObjects)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Overall_Percentage" -value $percent
  $SummaryHashTable = $SummaryHashTable + @{"Contacts that are mail-disabled (excluding ADFS Farm objects)" = $Summaryobj}
  $Summaryobj = $Null

  $Summaryobj = New-Object -TypeName PSObject
  $percent = "{0:P}" -f ($TotalDefaultUsersContainer/$ContactCount)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Item" -value ($Item = $Item + 1)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Statement" -value "Contacts left in the default Users container"
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Total_Count" -value $TotalDefaultUsersContainer
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Overall_Percentage" -value $percent
  $SummaryHashTable = $SummaryHashTable + @{"Contacts left in the default Users container" = $Summaryobj}
  $Summaryobj = $Null

  $Summaryobj = New-Object -TypeName PSObject
  $percent = "{0:P}" -f ($TotalUMIntegrationObjects/$ContactCount)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Item" -value ($Item = $Item + 1)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Statement" -value "Contacts that are UM Integration objects"
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Total_Count" -value $TotalUMIntegrationObjects
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Overall_Percentage" -value $percent
  $SummaryHashTable = $SummaryHashTable + @{"Contacts that are UM Integration objects" = $Summaryobj}
  $Summaryobj = $Null

  $Summaryobj = New-Object -TypeName PSObject
  $percent = "{0:P}" -f ($TotalADFSFarmObjects/$ContactCount)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Item" -value ($Item = $Item + 1)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Statement" -value "Contacts that are ADFS Farm objects"
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Total_Count" -value $TotalADFSFarmObjects
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Overall_Percentage" -value $percent
  $SummaryHashTable = $SummaryHashTable + @{"Contacts that are ADFS Farm objects" = $Summaryobj}
  $Summaryobj = $Null

  $Summaryobj = New-Object -TypeName PSObject
  $percent = "{0:P}" -f ($TotalConflictingObjects/$ContactCount)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Item" -value ($Item = $Item + 1)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Statement" -value "Contacts that are conflicting/duplicate objects"
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Total_Count" -value $TotalConflictingObjects
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Overall_Percentage" -value $percent
  $SummaryHashTable = $SummaryHashTable + @{"Contacts that are conflicting/duplicate objects" = $Summaryobj}
  $Summaryobj = $Null

  $Summaryobj = New-Object -TypeName PSObject
  $percent = "{0:P}" -f ($TotalExcludedObjects/$ContactCount)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Item" -value ($Item = $Item + 1)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Statement" -value "Contacts that have their `'$ExcludeAttribute`' attribute set to `'$ExcludeText`'"
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Total_Count" -value $TotalExcludedObjects
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Overall_Percentage" -value $percent
  $SummaryHashTable = $SummaryHashTable + @{"Contacts that have their `'$ExcludeAttribute`' attribute set to `'$ExcludeText`'" = $Summaryobj}
  $Summaryobj = $Null

  $Summaryobj = New-Object -TypeName PSObject
  $percent = "{0:P}" -f ($TotalExpirationTimeNotSet/$ContactCount)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Item" -value ($Item = $Item + 1)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Statement" -value "Contacts that have no expiration time set (expirationTime attribute)"
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Total_Count" -value $TotalExpirationTimeNotSet
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Overall_Percentage" -value $percent
  $SummaryHashTable = $SummaryHashTable + @{"Contacts that have no expiration time set (expirationTime attribute)" = $Summaryobj}
  $Summaryobj = $Null

  $Summaryobj = New-Object -TypeName PSObject
  $percent = "{0:P}" -f ($TotalManagerNotSet/$ContactCount)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Item" -value ($Item = $Item + 1)
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Statement" -value "Contacts that have no manager set (manager attribute)"
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Total_Count" -value $TotalManagerNotSet
  $Summaryobj | Add-Member -MemberType NoteProperty -Name "Overall_Percentage" -value $percent
  $SummaryHashTable = $SummaryHashTable + @{"Contacts that have no manager set (manager attribute)" = $Summaryobj}
  $Summaryobj = $Null

  write-host -ForegroundColor Green "Summary Totals:"

  $Output = $SummaryHashTable.values | ForEach {$_ } | ForEach {$_ } | Sort-Object Item
  $Output | Format-Table -AutoSize

  # Write-Output $Output | Format-Table
  $Output | Export-Csv -Path "$ReferenceFileSummaryTotals" -Delimiter $Delimiter -NoTypeInformation

  # Remove the quotes
  If ($RemoveQuotesFromCSV) {
    (get-content "$ReferenceFileSummaryTotals") |% {$_ -replace '"',""} | out-file "$ReferenceFileSummaryTotals" -Fo -En ascii
  }

  write-host "Notes:" -foregroundColor Yellow
  write-host " - Contacts are typically used to represent external users for the purpose of e-mail." -foregroundColor Yellow
  write-host " - Contacts can be 'disabled' by mail-disabling them." -foregroundColor Yellow
  write-host "   - Always clearly document situations where contacts have been disabled. i.e. Are they`n     being kept for a reason, such as for their phone details, etc, or should  they be`n     deleted. Delete them if they are no longer valid." -foregroundColor Yellow
  write-host " - Contacts should not be left in the default Users container (ie. CN=Users). Move them to`n   a more appropriate and meaningful location." -foregroundColor Yellow
  write-host " - Lync or OCS/Exchange Unified Messaging (UM) integration Objects are contact objects used`n   for Auto Attendant and Subscriber Access (Dial Plans) numbers for Enterprise Voice. Do`n   not treat them as a traditional contact object." -foregroundColor Yellow
  write-host " - ADFS Farm objects under the certificate sharing container need to be managed by the ADFS`n   team and not by this process. They are not a traditional contact object." -foregroundColor Yellow
  write-host " - A nice way to manage contacts is to set their expirationTime attribute. This will give`n   us the ability to implement a nice life cycle management process. You can go one step`n   further and add a user or mail enabled security group to the manager attribute. This`n   will give us the ability to implement some workflow when the contact is x days before`n   expiring." -foregroundColor Yellow

  write-host "`nCSV files to review:" -foregroundColor Yellow
  write-host " - $ReferenceFileFull" -foregroundColor Yellow
  write-host " - $ReferenceFileSummaryTotals" -foregroundColor Yellow

}

Enjoy!

Jeremy Saunders

Jeremy Saunders

Technical Architect | DevOps Evangelist | Software Developer | Microsoft, NVIDIA, Citrix and Desktop Virtualisation (VDI) Specialist/Expert | Rapper | Improvisor | Comedian | Property Investor | Kayaking enthusiast at J House Consulting
Jeremy Saunders is the Problem Terminator. He is a highly respected IT Professional with over 35 years’ experience in the industry. Using his exceptional design and problem solving skills with precise methodologies applied at both technical and business levels he is always focused on achieving the best business outcomes. He worked as an independent consultant until September 2017, when he took up a full time role at BHP, one of the largest and most innovative global mining companies. With a diverse skill set, high ethical standards, and attention to detail, coupled with a friendly nature and great sense of humour, Jeremy aligns to industry and vendor best practices, which puts him amongst the leaders of his field. He is intensely passionate about solving technology problems for his organisation, their customers and the tech community, to improve the user experience, reliability and operational support. Views and IP shared on this site belong to Jeremy.
Jeremy Saunders
Jeremy Saunders

Previous post:

Next post: