05 January 2017

PowerShell: Cached Exchange Mode Status Reporting

Recently, the firm I work at is going to cached exchange mode. Due to the sensitive nature of the industry I work in, we are turning on cached mode in blocks of users instead of all at once. We wanted to be able to track what machines have cached exchange mode turned on and what machines have it turned off. PowerShell and SCCM are the best method I devised.

I decided to write this PowerShell script in which it will report back to SCCM via WMI the status of cached exchange mode. It will iterate through all all HKEY_USER profiles to get the status of the exchange mailboxes and reports each to SCCM. The script will create a new WMI entry to write the status of cached exchange mode. In order for this to report to SCCM, you will need to import the WMI entry into the SCCM hardware inventory under Default Client Settings.

While writing this script, I decided to make it an option to either write the output to SCCM or to a text file for those admins that do not have SCCM. You can select a centralized location to write the text files to, which are named <ComputerName>.log with the status written inside the log.

The firm I am at skipped Office 2013, so I was not able to get the location and values of those keys. If you are wanting to use this with Office 2013, you will need to edit the Get-OfficeVersion and Find-RegistryKey functions to add those keys.

I have included examples on how to setup the script to execute at commandline within the script's .Example area. One more thing, SAPIEN's PowerShell Studio made writing this script a breeze! If it was not for PowerShell Studio, the script would not have the depth documentation it does.

This is what the report looked like in the SCCM Queries:


You will likely see some profiles saying unknown. This is caused by profiles that might have 2013 or Office 365 installed.

You can download the script from my GitHub site here.

CachedMode.ps1


1:  <#  
2:       .SYNOPSIS  
3:            Cached Exchange Mode Report  
4:         
5:       .DESCRIPTION  
6:            This script is to be executed on a machine to report if Microsoft Outlook is in cached exchange mode or not. It will report a status of on/off/unknown. The status was tracked down to registry key 00036601. The script queries under HKEY_USERS\%SID%\SOFTWARE\Microsoft\Office\16.0\Outlook\Profiles to find which key contains 00036601. The key under the above listed key is the name of the outlook profile and the key that contains 00036601 is a GUID key. Neither of those are standard across different systems, so the script has to find the actual key path. The script can then write the data to either a log file located at a centralized network path, or it can write it to the WMI so that it can be reported back to SCCM. The script was written so that it can be used in an environment that either has SCCM or does not. This script has not been written for Office 365 or Office 2013 as the firm I work at never used that version. If you need include Office 2013, you will need to add Office 15 to the Find-Registry Function  
7:         
8:       .PARAMETER SCCM  
9:            Write output to WMI for reporting to SCCM  
10:         
11:       .PARAMETER TextFile  
12:            Write output to a text file stored at a centralized repository  
13:         
14:       .PARAMETER TextFileLocation  
15:            Location to write the text file to  
16:         
17:       .EXAMPLE  
18:            Write output to WMI entry for reporting to SCCM  
19:                 powershell.exe -file CachedMode.ps1 -SCCM  
20:    
21:            Write output to a text file at a centralized location NOTE: -TextFileLocation can be prepopulated  
22:                 powershell.exe -file CachedMode.ps1 -TextFile -TextFileLocation '\\mick\Systems'  
23:    
24:       .NOTES  
25:            ===========================================================================  
26:            Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.3.131  
27:            Created on:       12/29/2016 12:45 PM  
28:            Created by:       Mick Pletcher  
29:            Organization:  
30:            Filename:         CachedModeReporting.ps1  
31:            ===========================================================================  
32:  #>  
33:  [CmdletBinding()]  
34:  param  
35:  (  
36:       [Switch]$SCCM,  
37:       [switch]$TextFile,  
38:       [string]$TextFileLocation  
39:  )  
40:    
41:  function Find-RegistryKey {  
42:  <#  
43:       .SYNOPSIS  
44:            Find Registry Key Value  
45:         
46:       .DESCRIPTION  
47:            Find the registry key that contains the specified value entry  
48:         
49:       .PARAMETER Value  
50:            Value to search registry key for  
51:         
52:       .PARAMETER SID  
53:            HKEY_USERS SID  
54:         
55:       .EXAMPLE  
56:            PS C:\> Find-RegistryKey  
57:         
58:       .NOTES  
59:            Additional information about the function.  
60:  #>  
61:         
62:       [CmdletBinding()][OutputType([string])]  
63:       param  
64:       (  
65:            [ValidateNotNullOrEmpty()][string]$Value,  
66:            [ValidateNotNullOrEmpty()][string]$SID  
67:       )  
68:         
69:       $Version = Get-OfficeVersion  
70:       switch ($Version) {  
71:            "Office 14" { $Key = "HKEY_USERS\" + $SID + "\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles" }  
72:            "Office 16" { $Key = "HKEY_USERS\" + $SID + "\SOFTWARE\Microsoft\Office\16.0\Outlook\Profiles" }  
73:       }  
74:       If ((Test-Path REGISTRY::$Key) -eq $true) {  
75:            [string]$CachedMode = get-childitem REGISTRY::$Key -recurse -ErrorAction SilentlyContinue | where-object { $_.property -eq "00036601" }  
76:            If ($CachedMode -ne $null) {  
77:                 [string]$CachedModeValue = (Get-ItemProperty REGISTRY::$CachedMode).'00036601'  
78:                 switch ($Version) {  
79:                      "Office 14" {  
80:                           switch ($CachedModeValue) {  
81:                                #Values below are converted to decimal from the registry hex value commented to the right  
82:                                '128 25 0 0' { Return "Enabled" } #'80 19 0 0'  
83:                                '0 16 0 0' { Return "Disabled" } #'0 10 0 0'  
84:                                default { Return "Unknown" }  
85:                           }  
86:                      }  
87:                      "Office 16" {  
88:                           switch ($CachedModeValue) {  
89:                                #Values below are converted to decimal from the registry hex value commented to the right  
90:                                '132 25 0 0' { Return "Enabled" } #'84 19 0 0'  
91:                                '4 16 0 0' { Return "Disabled" } #'4 10 0 0'  
92:                                default { Return "Unknown" }  
93:                           }  
94:                      }  
95:                 }  
96:                 Return $CachedModeValue  
97:            } else {  
98:                 Return $null  
99:            }  
100:       } else {  
101:            Return $null  
102:       }  
103:  }  
104:    
105:  function Get-HKEY_USERS_List {  
106:  <#  
107:       .SYNOPSIS  
108:            Retrieve list of HKEY_Users  
109:         
110:       .DESCRIPTION  
111:            Retrieve list of HKEY_Users while excluding the built-in and administrator accounts  
112:         
113:       .EXAMPLE  
114:                      PS C:\> Get-HKEY_USERS_List  
115:         
116:       .NOTES  
117:            Additional information about the function.  
118:  #>  
119:         
120:       [CmdletBinding()][OutputType([array])]  
121:       param ()  
122:         
123:       #Get list of HKEY_USERS registry keys filtering out built-in users  
124:       $HKEY_USERS = Get-ChildItem REGISTRY::HKEY_USERS | where-object { ($_.Name -like "*S-1-5-21*") -and ($_.Name -notlike "*_Classes") }  
125:       $Users = @()  
126:       foreach ($User in $HKEY_USERS) {  
127:            #Get the SID of the first profile  
128:            $PROFILESID = Get-ChildItem REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" | Where-Object { $_.name -like "*" + $USER.PSChildName + "*" }  
129:            $SID = $PROFILESID.PSChildName  
130:            #Determine if cached mode is on or off  
131:            $CachedMode = Find-RegistryKey -Value "00036601" -SID $SID  
132:            If ($CachedMode -ne $null) {  
133:                 #Get the username associated with the SID  
134:                 $ProfileName = ((Get-ItemProperty REGISTRY::$PROFILESID).ProfileImagePath).Split("\")[2]  
135:                 #Write username and sid to object  
136:                 $SystemInfo = New-Object -TypeName System.Management.Automation.PSObject  
137:                 Add-Member -InputObject $SystemInfo -MemberType NoteProperty -Name Profile -Value $ProfileName  
138:                 Add-Member -InputObject $SystemInfo -MemberType NoteProperty -Name Status -Value $CachedMode  
139:                 $Users += $SystemInfo  
140:            }  
141:       }  
142:       Return $Users  
143:  }  
144:    
145:  function New-WMIClass {  
146:       [CmdletBinding()]  
147:       param  
148:       (  
149:            [ValidateNotNullOrEmpty()][string]$Class  
150:       )  
151:         
152:       $WMITest = Get-WmiObject $Class -ErrorAction SilentlyContinue  
153:       If ($WMITest -ne "") {  
154:            $Output = "Deleting " + $Class + " WMI class....."  
155:            Remove-WmiObject $Class  
156:            $WMITest = Get-WmiObject $Class -ErrorAction SilentlyContinue  
157:            If ($WMITest -eq $null) {  
158:                 $Output += "Success"  
159:            } else {  
160:                 $Output += "Failed"  
161:                 Exit 1  
162:            }  
163:            Write-Output $Output  
164:       }  
165:       $Output = "Creating " + $Class + " WMI class....."  
166:       $newClass = New-Object System.Management.ManagementClass("root\cimv2", [String]::Empty, $null);  
167:       $newClass["__CLASS"] = $Class;  
168:       $newClass.Qualifiers.Add("Static", $true)  
169:       $newClass.Properties.Add("Profile", [System.Management.CimType]::String, $false)  
170:       $newClass.Properties["Profile"].Qualifiers.Add("key", $true)  
171:       $newClass.Properties["Profile"].Qualifiers.Add("read", $true)  
172:       $newClass.Properties.Add("Status", [System.Management.CimType]::String, $false)  
173:       $newClass.Properties["Status"].Qualifiers.Add("key", $true)  
174:       $newClass.Properties["Status"].Qualifiers.Add("read", $true)  
175:       $newClass.Put() | Out-Null  
176:       $WMITest = Get-WmiObject $Class -ErrorAction SilentlyContinue  
177:       If ($WMITest -eq $null) {  
178:            $Output += "Success"  
179:       } else {  
180:            $Output += "Failed"  
181:            Exit 1  
182:       }  
183:       Write-Output $Output  
184:  }  
185:    
186:  function New-WMIInstance {  
187:  <#  
188:       .SYNOPSIS  
189:            Write new instance  
190:         
191:       .DESCRIPTION  
192:            Write a new instance for each profile along with its cached mode status  
193:         
194:       .PARAMETER Username  
195:            Username  
196:         
197:       .PARAMETER CachedModeStatus  
198:            Status of exchange cached mode  
199:         
200:       .PARAMETER Class  
201:            WMI Class to write information to  
202:         
203:       .PARAMETER MappedDrives  
204:            List of mapped drives  
205:         
206:       .EXAMPLE  
207:            PS C:\> New-WMIInstance  
208:         
209:       .NOTES  
210:            Additional information about the function.  
211:  #>  
212:         
213:       [CmdletBinding()]  
214:       param  
215:       (  
216:            [ValidateNotNullOrEmpty()][string]$Username,  
217:            [ValidateNotNullOrEmpty()][string]$CachedModeStatus,  
218:            [ValidateNotNullOrEmpty()][string]$Class  
219:       )  
220:         
221:       $Output = "Writing Cached Exchange information instance to" + [char]32 + $Class + [char]32 + "class....."  
222:       $Return = Set-WmiInstance -Class $Class -Arguments @{ Profile = $Username; Status = $CachedModeStatus }  
223:       If ($Return -like "*" + $Username + "*") {  
224:            $Output += "Success"  
225:       } else {  
226:            $Output += "Failed"  
227:       }  
228:       Write-Output $Output  
229:  }  
230:    
231:  function Get-OfficeVersion {  
232:  <#  
233:       .SYNOPSIS  
234:            Get Microsoft Office Version  
235:         
236:       .DESCRIPTION  
237:            Execute the OSPP.vbs to display the license information, which also contains the current version of Microsoft Office.  
238:         
239:       .EXAMPLE  
240:                      PS C:\> Get-OfficeVersion  
241:         
242:       .NOTES  
243:            Additional information about the function.  
244:  #>  
245:         
246:       [CmdletBinding()][OutputType([string])]  
247:       param ()  
248:         
249:       If ((Test-Path $env:ProgramFiles"\Microsoft Office") -eq $true) {  
250:            $File = get-childitem -path $env:ProgramFiles"\Microsoft Office" -filter ospp.vbs -recurse  
251:       }  
252:       If ((Test-Path ${env:ProgramFiles(x86)}"\Microsoft Office") -eq $true) {  
253:            $File = get-childitem -path ${env:ProgramFiles(x86)}"\Microsoft Office" -filter ospp.vbs -recurse  
254:       }  
255:       #Get current version of office  
256:       $Version = (cscript.exe $File.Fullname /dstatus | where-object { $_ -like "LICENSE NAME:*" }).split(":")[1].Trim().Split(",")[0]  
257:       Return $Version  
258:  }  
259:    
260:  function Initialize-HardwareInventory {  
261:  <#  
262:       .SYNOPSIS  
263:            Perform Hardware Inventory  
264:         
265:       .DESCRIPTION  
266:            Perform a hardware inventory via the SCCM client to report the WMI entry.  
267:         
268:       .EXAMPLE  
269:                      PS C:\> Initialize-HardwareInventory  
270:         
271:       .NOTES  
272:            Additional information about the function.  
273:  #>  
274:         
275:       [CmdletBinding()]  
276:       param ()  
277:         
278:       $Output = "Initiate SCCM Hardware Inventory....."  
279:       $SMSCli = [wmiclass] "\\localhost\root\ccm:SMS_Client"  
280:       $ErrCode = ($SMSCli.TriggerSchedule("{00000000-0000-0000-0000-000000000001}")).ReturnValue  
281:       If ($ErrCode -eq $null) {  
282:            $Output += "Success"  
283:       } else {  
284:            $Output += "Failed"  
285:       }  
286:       Write-Output $Output  
287:  }  
288:    
289:  Clear-Host  
290:  #Get list of users and report if they are running Outlook in cached exchange mode  
291:  $Users = Get-HKEY_USERS_List  
292:  If ($SCCM.IsPresent) {  
293:       #Create new WMI Class to report status to  
294:       New-WMIClass -Class "Cached_Exchange_Mode"  
295:       #Write Cached Mode Status reports to WMI instances  
296:       foreach ($User in $Users) {  
297:            New-WMIInstance -Username $User.Profile -CachedModeStatus $User.Status -Class "Cached_Exchange_Mode"  
298:       }  
299:       Initialize-HardwareInventory  
300:  }  
301:  If ($TextFile.IsPresent) {  
302:       #Check if $TextFileLocation is populated  
303:       If (($TextFileLocation -ne "") -and ($TextFileLocation -ne $null)) {  
304:            #Check if $TextFileLocation exists  
305:            If ((Test-Path $TextFileLocation) -eq $true) {  
306:                 #Insert backslash at the end of the $TextFileLocation and define the name of the text file  
307:                 If ($TextFileLocation.Length - 1 -ne '\') {  
308:                      $File = $TextFileLocation + '\' + $env:COMPUTERNAME + ".log"  
309:                 } else {  
310:                      $File = $TextFileLocation + $env:COMPUTERNAME + ".log"  
311:                 }  
312:                 #Delete the old log file if it exists  
313:                 If ((Test-Path $File) -eq $true) {  
314:                      Remove-Item $File -Force  
315:                 }  
316:                 #Write the results to the log file  
317:                 $Users | Out-File $File -Encoding UTF8 -Force  
318:            } else {  
319:                 Write-Host "Text file location does not exist"  
320:            }  
321:       } else {  
322:            Write-Host "No text file location was defined."  
323:       }  
324:  }  
325:  #Display Results to Screen  
326:  $Users