Thursday, May 19, 2011

Exporting the Membership of a Large, Nested, Active Directory Group

One of the most common requests I receive is to enumerate the membership of an Active Directory group and provide some basic information back about the members. Simple enough task if the group is flat and contains a limited number of members. Once you start dealing with nested groups and groups containing more than 1,500 members (1,000 members for an Active Directory 2000 Forest), you need to start planning how you output the information.

With groups containing more than 1,500 members, performing the following will not provide the entire population.
$groupObject = New-Object System.DirectoryServices.DirectoryEntry("GC://CN=All Employees,OU=Distribution Lists,DC=ad,DC=mydomain,DC=local")
foreach($member in $groupObject.member) { Write-Output $member }
You will only enumerate the first 1,500 distinguished names of the multivalued attribute 'member' and output them to the console. Big enough to convince you that you retrieved all the members on first glance but on further investigation you will realize you are missing some people. Hopefully, you caught that and not the person that requested the report with the resolved distinguished names. To overcome this limitation, you need to search within the group object itself and return a range of values within the 'member' attribute; looping until you have enumerated each and every distinguished name.

Active Directory groups can contain user objects, computer objects, contact objects and other groups. To obtain the full membership of a Security Group to understand who has rights to an asset or a Distribution List to understand who will receive an e-mail, you must be aware of nested groups. If a group is a member is a member of the initial group, you will need to enumerate that group and any other groups that it may contain and so on. One of the issues that you can encounter with nested groups is the Administrator that nested a group that is member of a group that itself is patriline to that group. This leads to looping in reporting and must be accounted when recursively enumerating the membership. I tackle this problem by storing the nested group distinguished names that were enumerated in an array and at each recursion evaluating if that distinguished name is an element.

The code sample below deals with these two issues related to group enumeration. I have tested this against code against a group that contained 32,000+ member objects with multiple layers of nesting genreating a 2.9 megabyte report. The only big deficit in the code is there is no provision to deal with objects that are of the foreignSecurityPrincipal class. If you have cross-forest membership in groups, you can add that to the if-then statement and add your own formatting function for those objects. A nice feature of this script is that if you need to enumerate a large number of groups that have similar sAMAccountNames you can wild card them in the command line argument, such as "-group *AdSales*" or "-group DL_*". The code in the script is fairly portable. You can recombine these functions to enumerate the groups associated to NTFS security to provide an access report. You just need to update the functions that deal with the formatting of the objects to provide the desired information.
param([string]$domain,[string]$group,[switch]$verbose)
#-------------------------------------------------------------------------------------------------#
Function Find-GroupDistinguishedNamesBysAMAccountName($domain,$sAMAccountName) {
 $groupDistinguishedNames = @()
 $directorySearcher = New-Object System.DirectoryServices.DirectorySearcher
 $directorySearcher.SearchRoot = (New-Object System.DirectoryServices.DirectoryEntry(("LDAP://" + (Get-LocalDomainController $domain) + "/" + (Get-DomainDn $domain))))
 $directorySearcher.Filter = "(&(objectClass=group)(objectCategory=group)(sAMAccountName=$sAMAccountName))"
 $directorySearcher.PropertiesToLoad.Clear()
 $directorySearcher.PropertiesToLoad.Add("distinguishedName") | Out-Null
 $searchResults = $directorySearcher.FindAll()

 if($searchResults -ne $null) {
  foreach($searchResult in $searchResults) {
   if($searchResult.Properties.Contains("distinguishedName")) {
    $groupDistinguishedNames += $searchResult.Properties.Item("distinguishedName")[0]
   }
  }
 } else {
  $groupDistinguishedNames = $null
 }
 $directorySearcher.Dispose()
 return $groupDistinguishedNames
}
#-------------------------------------------------------------------------------------------------#
Function Get-GroupType($groupType) {
 if($groupType -eq -2147483646) {
  $groupType = "Global Security Group"
 } elseif($groupType -eq -2147483644) {
  $groupType = "Domain Local Security Group"
 } elseif($groupType -eq -2147483643) {
  $groupType = "BuiltIn Group"
 } elseif($groupType -eq -2147483640) {
  $groupType = "Universal Security Group"
 } elseif($groupType -eq 2) {
  $groupType = "Global Distribution List"
 } elseif($groupType -eq 4) {
  $groupType = "Local Distribution List"
 } elseif($groupType -eq 8) {
  $groupType = "Universal Distribution List"
 } else {
  $groupType = "Unknown Group Type"
 }
 return $groupType
}
#-------------------------------------------------------------------------------------------------#
Function Get-GroupMembers($distinguishedName,$level,$groupsReported) {
 $reportText = @()
 $groupObject = New-Object System.DirectoryServices.DirectoryEntry("GC://" + ($distinguishedName -replace "/","\/"))
 if($groupObject.groupType[0] -ne -2147483640 -or $groupObject.groupType[0] -ne 8) {
  $groupObject = New-Object System.DirectoryServices.DirectoryEntry("LDAP://" + (Get-LocalDomainController(Get-ObjectAdDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
 }
 $directorySearcher = New-Object System.DirectoryServices.DirectorySearcher($groupObject)
 $lastQuery = $false
 $quitLoop = $false
 if($groupObject.member.Count -ge 1000) {
  $rangeStep = 1000
 } elseif($groupObject.member.Count -eq 0) {
  $lastQuery = $true
  $quitLoop = $true
 } else {
  $rangeStep = $groupObject.member.Count
 }
 $rangeLow = 0
 $rangeHigh = $rangeLow + ($rangeStep - 1)
 $level = $level + 2
 while(!$quitLoop) {
  if(!$lastQuery) {
   $attributeWithRange = "member;range=$rangeLow-$rangeHigh"
  } else {
   $attributeWithRange = "member;range=$rangeLow-*"  }
  $directorySearcher.PropertiesToLoad.Clear()
  $directorySearcher.PropertiesToLoad.Add($attributeWithRange) | Out-Null
  $searchResult = $directorySearcher.FindOne()
  $directorySearcher.Dispose()
  if($searchResult.Properties.Contains($attributeWithRange)) {
   foreach($member in $searchResult.Properties.Item($attributeWithRange)) {
    $memberObject = Get-ActiveDirectoryObject $member
    if($memberObject.objectClass -eq "group") {
     $reportText += Format-Group $memberObject $level $groupsReported
    } elseif ($memberObject.objectClass -eq "contact") {
     $reportText += Format-Contact $memberObject $level
    } elseif ($memberObject.objectClass -eq "computer") {
     $reportText += Format-Computer $memberObject $level
    } elseif ($memberObject.objectClass -eq "user") {
     $reportText += Format-User $memberObject $level
    } else {
     Write-Warning "NOT SUPPORTED: $member"
    }
   }
   if($lastQuery) { $quitLoop = $true }
  } else {
   $lastQuery = $true
  }
  if(!$lastQuery) {
   $rangeLow = $rangeHigh + 1
   $rangeHigh = $rangeLow + ($rangeStep - 1)
  }
 }
 return $reportText
}
#-------------------------------------------------------------------------------------------------#
Function Format-User($userObject,$level) {
 $reportText = @()
 if($userObject.displayName) {
  $identity = ((" " * $level) + $userObject.displayName.ToString() + " [" + (Get-ObjectNetBiosDomain $userObject.distinguishedName) + "\" + $userObject.sAMAccountName.ToString() + "]")
 } else {
  $identity = ((" " * $level) + $userObject.name.ToString() + " [" + (Get-ObjectNetBiosDomain $userObject.distinguishedName) + "\" + $userObject.sAMAccountName.ToString() + "]")
 }
 if($userObject.mail) {
  $identity = ("$identity <" + $userObject.mail.ToString() + ">")
 }
 if($userObject.userAccountControl[0] -band 0x0002) {
  $identity = "$identity (User Disabled)"
 } else {
  $identity = "$identity (User Enabled)"
 }
 $reportText += $identity
 $description = ((" " * $level) + "  Description: " + $userObject.description.ToString())
 $reportText += $description
 if($verbose) { Write-Host ($reportText | Out-String) }
 return $reportText
}

Function Format-Contact($contactObject,$level) {
 $reportText = @()
 $identity = ((" " * $level) + $contactObject.displayName.ToString() + " [" + (Get-ObjectNetBiosDomain $contactObject.distinguishedName) + "] <" + $contactObject.mail.ToString() + "> (Contact)")
 $description = ((" " * $level) + "  Description: " + $contactObject.description.ToString())
 $reportText += $identity
 $reportText += $description
 if($verbose) { Write-Host ($reportText | Out-String) }
 return $reportText
}

Function Format-Computer($computerObject,$level) {
 $reportText = @()
    $identity = ((" " * $level) + $computerObject.name + " [" + (Get-ObjectNetBiosDomain $computerObject.distinguishedName) + "\" + $computerObject.sAMAccountName.ToString() + "] (Computer)")
 $operatingSystem = ((" " * $level) + "  OS: " + $computerObject.operatingSystem.ToString())
 $reportText += $identity
    if($computerObject.dNSHostName) {
     $fqdn = ((" " * $level) + "  FQDN: " + ($computerObject.dNSHostName.ToString()).ToLower())
  $reportText += $fqdn
  $reportText += $operatingSystem
    } else {
  $reportText += $operatingSystem
    }
 if($verbose) { Write-Host ($reportText | Out-String) }
 return $reportText
}

Function Format-Group($groupObject,$level,$groupsReported) {
 $reportText = @()
 $identity = ((" " * $level) + $groupObject.name.ToString() + " [" + (Get-ObjectNetBIOSDomain $groupObject.distinguishedName) + "\" + $groupObject.sAMAccountName.ToString() + "]")
 if($groupObject.mail) {
  $identity = ("$identity <" + $groupObject.mail.ToString() + ">")
 }
 $identity = ("$identity (" + (Get-GroupType $groupObject.groupType) + ")")
 $reportText += $identity
 if($verbose) { Write-Host ($reportText | Out-String) }
 if($groupsReported -notcontains $groupObject.distinguishedName) {
  $groupsReported += $groupObject.distinguishedName
  $reportText += Get-GroupMembers $groupObject.distinguishedName.ToString() $level $groupsReported
 } else {
  $reportText += ((" " * $level) + "  [previously reported nested group]")
 }
 $reportText += ""
 return $reportText
}
#-------------------------------------------------------------------------------------------------#
Function Get-AllForestDomains {
 $domains = @()
 $forestInfo = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
 foreach($domain in $forestInfo.Domains) {
  $domains += $domain.name
 }
 return $domains
}

Function Get-DomainDn($domain) {
 return ((New-Object System.DirectoryServices.DirectoryEntry("LDAP://$domain/RootDSE")).defaultNamingContext).ToString()
}

Function Get-ObjectAdDomain($distinguishedName) {
 return ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
}

Function Get-ObjectNetBiosDomain($distinguishedName) {
 return ((Get-ObjectAdDomain $distinguishedName).Split(".")[0]).ToUpper()
}

Function Get-LocalDomainController($domain) {
 return ((New-Object System.DirectoryServices.DirectoryEntry("LDAP://$domain/RootDSE")).dnsHostName).ToString()
}

Function Get-ActiveDirectoryObject($distinguishedName) {
 return New-Object System.DirectoryServices.DirectoryEntry("LDAP://" + (Get-LocalDomainController (Get-ObjectAdDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
}
#-------------------------------------------------------------------------------------------------#
Set-Variable -name reportsDirectory -option Constant -value "$pwd\Reports"
Set-Variable -name forestDomains -option Constant -value @(Get-AllForestDomains)
#-------------------------------------------------------------------------------------------------#
if(!([bool]$domain) -or !([bool]$group)) {
 Write-Host ("  Example: .\Export-GroupMembership.ps1 -domain " + $forestDomains[0] + " -group Administrators -verbose") -foregroundcolor Yellow
 Write-Host ""
}

if(!([bool]$domain)) {
 Write-Host " You are missing the `"-domain`" Switch" -Foregroundcolor Red
 Write-Host ""
 Write-Host " The domain of the group."
 Write-Host "  Valid Domains:"
 foreach($forestDomain in $forestDomains) {
  Write-Host ("   $forestDomain [" + ($forestDomain.Split("."))[0] + "]")
 }
 Write-Host ""
 Write-Host " Please enter the Domain below"
 while(!([bool]$domain)) {
  $domain = Read-Host -prompt "`tDomain"
 }
 Write-Host ""
}

if(!([bool]$group)) {
 Write-Host " You are missing the `"-group`" Switch" -Foregroundcolor Red
 Write-Host ""
 Write-Host " Please enter the group name below"
 while(!([bool]$group)) {
  $group = Read-Host -prompt "`tGroup"
 }
 Write-Host ""
}

$validDomain = $false
foreach($forestDomain in $forestDomains) {
 if($forestDomain -eq $domain) {
  $validDomain = $true
  break
 }
 if((($forestDomain.Split("."))[0]) -eq $domain) {
  $validDomain = $true
  break
 }
}

if($validDomain -eq $false) {
 Write-Host ""
 Write-Host "$domain is not a valid domain in your current forest." -foregroundcolor Red
 Write-Host ""
 exit
}

if(!(Test-Path -path $reportsDirectory)) {
 New-Item -path $reportsDirectory -type Directory | Out-Null
}

$groupDistinguishedNames = Find-GroupDistinguishedNamesBysAMAccountName $domain $group

if($groupDistinguishedNames -eq $null) {
 Write-Host "Unable to locate $domain\$group. Exiting..." -foregroundColor Red
 exit
}

foreach($groupDistinguishedName in $groupDistinguishedNames) {
 $groupObject = Get-ActiveDirectoryObject $groupDistinguishedName
 $groupName = $groupObject.name.ToString()
 $groupsAMAccountName = $groupObject.sAMAccountName.ToString()
 $groupDomain = Get-ObjectAdDomain $groupDistinguishedName
 $groupNetBiosDomain = Get-ObjectNetBiosDomain $groupDistinguishedName
 $groupType = Get-GroupType $groupObject.groupType
 $outputFilename = ($groupDomain + "-" + $groupsAMAccountName + ".txt")
 $reportText = @()
 $reportText += ("#" + ("-" * 78) + "#")
 $reportText += "  Members of $groupName [$groupNetBiosDomain\$groupsAMAccountName]"
 $reportText += ("  Description: " + $groupObject.description.ToString())
 if($groupObject.mail -and $groupType -match "Distribution") {
  $reportText += "  Distribution List E-Mail Address: " + $groupObject.mail.ToString()
 } elseif($groupObject.mail) {
  $reportText += "  Security Group E-Mail Address: " + $groupObject.mail.ToString()
 }
 $reportText += "  Group Type: $GroupType"
 $reportText += ("  Report generated on " + (Get-Date -format D) + " at " + (Get-Date -format T))
 $reportText += ("#" + ("-" * 78) + "#")
 if($verbose) { Write-Host ($reportText | Out-String) }
 $reportText += Get-GroupMembers $groupDistinguishedName 0 @()
 $reportText += ("#" + ("-" * 78) + "#")
 if($verbose) { Write-Host ("#" + ("-" * 78) + "#") }
 Set-Content -path "$reportsDirectory\$outputFilename" -value ($reportText | Out-String)
}

Wednesday, May 18, 2011

Correct Order to Start and Stop BlackBerry Enterprise Server Services Using PowerShell

A common activity I perform after patching Exchange Servers and Domain Controllers is to restart BlackBerry services on client BlackBerry servers. Failure to do so can lead to issues for BlackBerry users such as failed delivery, lookup failures and inability to send. RIM recommends a specific order for these services to be stopped and started. Since patching occurs typically occurs in the dead of night to avoid impacting end users, you want to get things done quickly but accurately so you can get some sleep. In the code samples below, I take advantage of Get-Service, Stop-Service and Start-Service to execute the correct order of the process. I store the required service order in an array and take advantage of the natural looping order of elements in "foreach" to manipulate the services. The remaining services are discovered by obtaining all services with the name BlackBerry and filtering out the services that required a specific order for stopping and starting. If there are any issues with stopping or stopping the services, the code immediately exits so you can take manual intervention, research and resolve the problem.

Stopping BlackBerry Services:
#--------------------------------------------------------------------------------------------------#
Set-Variable -name selectedServices -option Constant -value @("BlackBerry Controller","BlackBerry Dispatcher","BlackBerry Router")
#--------------------------------------------------------------------------------------------------#
foreach($selectedService in $selectedServices) {
 if((Get-Service -Name $selectedService).Status -ne "Stopped") {
  Write-Output "Stopping $selectedService..."
  Stop-Service -Name $selectedService
  if((Get-Service -Name $selectedService).Status -ne "Stopped") {
   Write-Output "$selectedService failed to stop. Exiting..."
   exit
  } else {
   Write-Output "$selectedService successfully stopped."
  }
 } else {
  Write-Output "$selectedService already stopped!"
 }
}
foreach($blackberryService in (Get-Service -Name Blackberry*)) {
 if($selectedServices -notcontains $blackberryService.Name) {
  if($blackberryService.Status -ne "Stopped") {
   Write-Output ("Stopping " + $blackberryService.Name + "...")
   Stop-Service -Name $blackberryService.Name
   if((Get-Service -Name $blackberryService.Name).Status -ne "Stopped") {
    Write-Output ($blackberryService.Name + " failed to stop. Exiting...")
    exit
   } else {
    Write-Output ($blackberryService.Name + " successfully stopped.")
   }
  } else {
   Write-Output ($blackberryService.Name + " already stopped!")
  }
 }
}
Starting BlackBerry Services:
#--------------------------------------------------------------------------------------------------#
Set-Variable -name selectedServices -option Constant -value @("BlackBerry Router","BlackBerry Dispatcher","BlackBerry Controller")
#--------------------------------------------------------------------------------------------------#
foreach($selectedService in $selectedServices) {
 if((Get-Service -Name $selectedService).Status -ne "Running") {
  Write-Output "Starting $selectedService..."
  Start-Service -Name $selectedService
  if((Get-Service -Name $selectedService).Status -ne "Running") {
   Write-Output "$selectedService failed to start. Exiting..."
   exit
  } else {
   Write-Output "$selectedService successfully started."
  }
 } else {
  Write-Output "$selectedService already running!"
 }
}
foreach($blackberryService in (Get-Service -Name Blackberry*)) {
 if($selectedServices -notcontains $blackberryService.Name) {
  if($blackberryService.Status -ne "Running") {
   Write-Output ("Starting " + $blackberryService.Name + "...")
   Start-Service -Name $blackberryService.Name
   if((Get-Service -Name $blackberryService.Name).Status -ne "Running") {
    Write-Output ($blackberryService.Name + " failed to start. Exiting...")
    exit
   } else {
    Write-Output ($blackberryService.Name + " successfully started.")
   }
  } else {
   Write-Output ($blackberryService.Name + " already running!")
  }
 }
}

Tuesday, May 10, 2011

Build a Report of Unused BlackBerry Devices

A crucial element of BlackBerry management is reducing the cost of providing BlackBerry service. An unused BlackBerry is a wasted license, an unnecessary cell phone bill with a data plan and possibly a security issue. When thousands of handhelds are under management, dozens can be idle. In the current economic climate, every penny counts in your IT budget. Finding these idle devices can add up to thousands of dollars in savings annually.

In the code sample below, I build upon a previous blog post, "BlackBerry User Report". The command text is updated to select information from the UserStats table where the connection data for the BlackBerry devices is stored. If no value is held in the row for connectivity, a DBNull value will be returned. This is not the same as PowerShell's built-in "$null" and you will need to test against the DBNull class to determine this. The end result of this code example is a comma separated values text file with the employees neglecting their BlackBerry. Reach out to them and see if they no longer need their handheld or if they have moved on to an ActiveSync device as their primary mobile messaging device.

In a change from previous blog posts, I am using System.DirectoryServices.DirectorySearcher class to perform the user object lookup instead of ActiveX Data Object (ADO). I have a tendency to use ADO as the methodology is the same in PowerShell, Perl and VBScript making the translation of ADSI queries between languages simple.
Function Get-ForestRootDn {
 return ((New-Object System.DirectoryServices.DirectoryEntry("LDAP://RootDSE")).rootDomainNamingContext).ToString()
}

Function Get-ObjectAdDomain($distinguishedName) {
 return ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
}

Function Find-ActiveDirectoryObjectByEMailAddress($smtpAddress) {
 $directorySearcher = New-Object System.DirectoryServices.DirectorySearcher
 $directorySearcher.SearchRoot = (New-Object System.DirectoryServices.DirectoryEntry("GC://$forestRootDn"))
 $directorySearcher.Filter = "(&(objectClass=user)(objectCategory=person)(proxyAddresses=smtp:$smtpAddress))"
 $searchResult = $directorySearcher.FindOne()
 if($searchResult -ne $null) {
  $userObject = New-Object System.DirectoryServices.DirectoryEntry($searchResult.Path)
 } else {
  $userObject = $null
 }
 $directorySearcher.Dispose()
 return $userObject
}
#--------------------------------------------------------------------------------------------------#
Set-Variable -name forestRootDn -option Constant -scope Script -value (Get-ForestRootDn)
Set-Variable -name dbNull -option Constant -value ([System.DBNull]::Value) # http://msdn.microsoft.com/en-us/library/system.dbnull.aspx
Set-Variable -name databases -option Constant -value @("mssql2008-east.ad.mydomain.local","mssql2008-west.ad.mydomain.local","mssql2008-europe.ad.mydomain.local","mssql2008-japan.ad.mydomain.local")
Set-Variable -name idleDays -option Constant -value 14
#--------------------------------------------------------------------------------------------------#
$blackberryUsers = @()

foreach($sqlServer in $databases) {
 $sqlConnection = New-Object System.Data.SQLClient.SQLConnection
 $sqlConnection.ConnectionString = "server=$sqlServer;database=BESMgmt;trusted_connection=true;"
 $sqlConnection.Open()
 
 $sqlCommand = New-Object System.Data.SQLClient.SQLCommand
 $sqlCommand.Connection = $sqlConnection
 $sqlCommand.CommandText = "SELECT u.DisplayName as DisplayName, u.MailboxSMTPAddr as MailboxSMTPAddr, u.PIN as PIN, d.phonenumber as phonenumber, s.MachineName as MachineName, t.LastFwdTime as LastFwdTime, t.LastSentTime as LastSentTime, t.MsgsPending as MsgsPending FROM UserConfig u, SyncDeviceMgmtSummary d, ServerConfig s, UserStats t WHERE u.id = d.userconfigid and u.ServerConfigId = s.id and u.id = t.userconfigid"
 
 $sqlDataReader = $sqlCommand.ExecuteReader()
 
 if($sqlDataReader.HasRows -eq $true) {
  while($sqlDataReader.Read()) {
   $displayName = $sqlDataReader.Item("DisplayName")
   $proxyAddress = $sqlDataReader.Item("MailboxSMTPAddr")
   $pin = $sqlDataReader.Item("PIN")
   $phoneNumber = $sqlDataReader.Item("phonenumber")
   $blackberryServer = $sqlDataReader.Item("MachineName")
   $lastFwdTime = ($sqlDataReader.Item("LastFwdTime"))
   $lastSentTime = $sqlDataReader.Item("LastSentTime") 
   $msgsPending = $sqlDataReader.Item("MsgsPending")
 
   if($lastSentTime -eq $dbNull) { $lastSentTime = "No Contact" } else { $lastSentTime = (Get-Date $lastSentTime) }
   if($lastFwdTime -eq $dbNull) { $lastFwdTime = "No Contact" } else { $lastFwdTime = (Get-Date $lastFwdTime) }
   
   if(($lastSentTime -eq "No Contact" -or $lastFwdTime -eq "No Contact") -or ((Get-Date $lastSentTime) -le (Get-Date).AddDays(-$idleDays) -or (Get-Date $lastFwdTime) -le (Get-Date).AddDays(-$idleDays))) {
    $userObject = Find-ActiveDirectoryObjectByEMailAddress $proxyAddress
    if($userObject -eq $null) {
     Write-Host "USER NOT FOUND: $proxyAddress" -foregroundColor Red
    } else {
     $blackberryUser = New-Object -typeName PSObject
     Add-Member -inputObject $blackberryUser -type NoteProperty -name "Domain" -value ((Get-ObjectAdDomain $userObject.distinguishedName.ToString()).Split(".")[0]).ToUpper()
     Add-Member -inputObject $blackberryUser -type NoteProperty -name "User ID" -value $userObject.sAMAccountName.ToString()
     Add-Member -inputObject $blackberryUser -type NoteProperty -name "First Name" -value $userObject.givenName.ToString()
     Add-Member -inputObject $blackberryUser -type NoteProperty -name "Last Name" -value $userObject.sn.ToString()
     Add-Member -inputObject $blackberryUser -type NoteProperty -name "Display Name" -value $userObject.displayName.ToString()
     Add-Member -inputObject $blackberryUser -type NoteProperty -name "E-Mail Address" -value $userObject.mail.ToString()
     Add-Member -inputObject $blackberryUser -type NoteProperty -name "Last Message Forwarded" -value $lastFwdTime
     Add-Member -inputObject $blackberryUser -type NoteProperty -name "Last Message Sent" -value $lastSentTime
     Add-Member -inputObject $blackberryUser -type NoteProperty -name "Messages Pending" -value $msgsPending
     Add-Member -inputObject $blackberryUser -type NoteProperty -name "PIN" -value $pin
     Add-Member -inputObject $blackberryUser -type NoteProperty -name "Cell Phone Number" -value $phoneNumber
     Add-Member -inputObject $blackberryUser -type NoteProperty -name "Desk Phone Number" -value $userObject.telephoneNumber.ToString()
     Add-Member -inputObject $blackberryUser -type NoteProperty -name "Street Address" -value ($userObject.streetAddress.ToString() -replace "`r`n",", ")
     Add-Member -inputObject $blackberryUser -type NoteProperty -name "City" -value $userObject.l.ToString()
     Add-Member -inputObject $blackberryUser -type NoteProperty -name "State" -value $userObject.st.ToString()
     Add-Member -inputObject $blackberryUser -type NoteProperty -name "Zip Code" -value $userObject.postalCode.ToString()
     Add-Member -inputObject $blackberryUser -type NoteProperty -name "Country" -value $userObject.c.ToString()
     Add-Member -inputObject $blackberryUser -type NoteProperty -name "BlackBerry Server" -value $blackberryServer
     $blackberryUsers += $blackberryUser
    }
   }
  }
 }
 $sqlConnection.Close()
}

$blackberryUsers | Export-Csv -path "Stale Blackberry User Report.csv" -noTypeInformation