Thursday, October 6, 2016

One Month With Android

WP7 Looking rather toyish now

I love my Windows Phone. 

I was one of the two people standing outside my local AT&T store to get it on release day. My very first smartphone was a Samsung Windows Mobile 7.0. I skipped all other smart devices, turned in my Motorola flip phone and pocketed one of these.. 

Here was such a dramatic change on cell phones seen in a LONG time. Live tiles, Xbox integration, deep in the Microsoft environment, and best of all, no stupid wall of icons (ala Windows 3.1).

Inspiration for iPhone and Android OS.
As the OS matured, it only got better. Tile sizes started ranging from small 1/4th size, to a large 16x grid. Live tiles are absolutely awesome! Weather? 96F. Unread message from my boss in my inbox. My son just got another XBox achievement. Meeting in hour in big conference room. Amazon order just arrived on my porch. My wife has just updated her Facebook profile. Best of all, I only logged into my home screen. "2-click access".

With the OS, came some really interesting phones. My absolute favorite being the Lumia 1020 with it's glorious 41 megapixel camera. A camera that put my 10-year old DSLR to shame for basic, everyday photography. It's only real drawback was the lack of a microSD card slot, to hold those 20mb images. I loved this camera. I went as far as to purchase the extended battery case.


Trips to Disneyland were awesome because I only had to carry my cell to get a big camera; all-in-one, great low-light images, very easy package to carry around. The added battery, plus an real tripod mount on my phone made it possible to take some awesome low-light images.

Tower of Terror via Lumia 1020 from article TechBuffalo

Sadly, the honeymoon ended..  

Lumia, the premier windows phone manufacturer was gobbled up, disassembled and soon afterwards, all their production shut down. We've had 3 mediocre (IMO) phones put out. 640, 650, and 950. A few other's, like HP put a toe in the water and spun up the Elite x3, which I was really excited about, but it isn't going to fix things.

Things like, apps. Unless you're my uncle, who 'only uses his phone for calls', apps play an important role on any modern handheld. I understand, programmatically the pains of supporting a platform. (i.e. scripts that no longer work due to new technologies, scripts that rely on an API that is barely supported by the vendor.) Having not just one, two, but three different operating systems and virtually hundreds of different hardware platforms is more than daunting. A developer's trepidation to simply step into a third handheld arena is palpable. Can you say 'third-wheel'?

Android (aka the suit), and Apple (the cheerleader) hanging with Microsoft (the nerd).

Why go with 'The Suit'? 

Two reasons. I am already one foot into the google landscape. This blog is hosted blogger.com, which is ran by Google. Second, while Apple does have some nice looking hardware, she is very high maintenance. All about the bling. Android is at least grounded in a bit of reality, having reasonably priced hardware. In addition, the OS is 'fairly open'. It's not impossible to customize the loader, or change the search engine to meet my eccentric whims.
I started off with a work paid-for Galaxy S5. I had this for about a week for testing the deployment of new MDM solution in-house. During the week that I had this, I enjoyed being able to play Pokemon Go, stream music via the Amazon Prime app, watch tv via the Xfinity TV app, and even run Disney Appisodes for my toddler. Apps, apps and more apps. Kid in a candy store and all for free!!

Meanwhile on my Windows Mobile: Amazon pulls their app and replaces it with pitiful web skin. Battery on my Lumia 1520 is topping out at 2 hours on fast ring build in stand-by. Pokemon Go had my 10 year old wanting to walk the half mile to the neighborhood park. 

Even better, with a little finagling, I was able to 'win-mobile'-ify my phone. I tried looking for a decent 'live tiles' loader, but none had the real flair of Windows mobile 10, maybe WP7 where there are only 2 button sizes. Bleh. Nova Launcher provided both a sorted app list, and app grouping (which is 70% of what I missed). Home screen now has three groups, 'entertainment', 'games' and 'work' plus shortcut to my wife's contact. Next step, Cortana. Holding down the home key brings up the Cortana (beta) and I get my familiar search functionality (plus my fka Bing rewards for searches). In addition, I get alerts on my Win10PC about new SMS messages, battery low, etc.. The lousy part is I've broken voice searching, 'OK Google' nothing and 'Hey Cortana' only works from inside the app.

After the testing, I returned the phone and then seriously started considering Android as an option. A co-worker presented me with his 'old' work phone. He has a Samsung Galaxy S6 that he wasn't using at the moment and I could long-term borrow it. OK! Pulling the sim from my Lumia, I started rebuilding this phone. Only this time, adding widgets for weather, (Outlook) calendar and Audible.

Android Pain points:

WP Media Controls 

  • Full media control from lock screen - WP has this awesome option that if I tap the physical volume buttons, the system's media interface drops down, then I can pause currently playing media (audible, spotify, etc.). Android appears to not like that and it requires a widget on the screen to give you basic lock-screen functionality. 
  • The land of micro-transaction - Do you want weather with no ads? $2/month. Want spam call blocking? $1/month Want a new theme for your phone (no bing image of day)? $1.50. While I was used to some of this on Windows Phone, the Android market place is more wild-west. You can find a hundred free apps, each with a multitude levels of microtransaction built into it. 
  • Loss of live tiles - Widget have helped with this but it's simply not as elegant. Clicking on an appointment on my work calendar doesn't play well with the corporate MDM policies. I often get a 'your corporate policy doesn't allow this' notification because of 'crossing boundaries'. When it was the Outlook live tile providing the meeting info, it didn't complain. 
  • Bluetooth support - I was living the wireless dream. Sit down in my car, put it on the charger base, start streaming audio. Phone played music/audio via bluetooth connection to my car stereo. Unfortunately, this S6 does not work at all. The bluetooth at best will play 10 seconds of audio, then reset the connection. This will either restart the audio, rewind 5 seconds or stop playback altogether. I'm forced into using a wired 'auxiliary' cable to connect my phone to my stereo. 
  • Bloatware and nagging native apps - it was annoying on Windows that the native apps would reinstall after a full rebuild, but I COULD uninstall all of them if I wanted to. That ISP based navigation system that requires a special subscription? Gone. Family tracking app when only my wife has a phone? Gone. Android on the other hand forces me to keep all of these apps and more. In fact, it makes it REALLY hard to not use them, I continue to get notifications that the native email client needs my credentials for my work email. Guess what, my employer won't let the native app sync, we're an O365 shop, so it will NEVER work. Stop asking me and go away.. 
Amazon sees my giant water glass.

Android Perks:

  • Richer apps - Not only is the Android Amazon app not a web-skin, but it has awesome object scanning functionality. Point the scanner at the a barcode and it quickly find the item. Point the scanner at the water glass on my desk, it returns keywords related to that item. 
  • Richer accessory ecosystem - Search Amazon for a Galaxy S6 (or work sponsored S5) case and it returns 1.6 million options. Search for my Lumia 1520, you don't get even 1% of the options.
  • Microsoft apps still work - Cortana is here for my searches. OneDrive is still backing up my photos. Word, Excel, Outlook are available for my work

Microsoft, do you want me back? Become more Disney.

While, I've quit using my Windows Phone, I still prefer the OS. Win mobile has character. Because it's a brand new OS, you've customized beyond the other two options, adding little tidbits/flavor items. I kind of think of you as a renaissance faire. Tons of character, limitless customization options and your biggest draw is homegrown apps by third party 'entertainers'. On the other hand, Android is like Disneyland. There is no originality, no individuality, no character, but they have a TON of flash, bang and glitz. Fireworks, movie-themed amusement rides and churros draw in a lot more people than jousting tournaments and turkey legs. 

I want Disneyland on my terms. I want a phone to brag about with more flavor of Windows Mobile. I need you to be enthusiastic about selling the phone. That will make people more enthusiastic to buy it and people enthusiastic to develop for it. You and only you are going to break down that app wall. I like that the Amazon app can scan something without a barcode and return results close to it.

How to get there ... 

Windows Mobile's future if no consumer focus. (NYTimes)
Surface phone better come true. It better be awesome, ground breaking and absolutely shiny with all those bells and whistles that that "Surface" name implies. Phone case with BT keyboard? Wireless charging?

Bring back the Android bridge. This will help because we get access to the apps, and developers don't have to design for 3 different OS. You have to realize that if you're target audience is only corporate, you are becoming RIM and what's their latest phone? None. They plan to give up their 0.01% handheld marketshare to focus on MDM solutions. At least Microsoft, you have Office/o365 and Windows on desktop, I suppose there's always that.


Monday, June 27, 2016

Recurse Groups - All Native Tools

Found this post from 2011 that used a slick directory searcher to find group membership. While it doesn't give parents, I was able to simply loop until I went up each chain.
Function AllMemberOf ($SamAccountName) {
# Based off of 
#  http://stackoverflow.com/questions/5072996/how-to-get-all-groups-that-a-user-is-a-member-of
 $groups = ([ADSISEARCHER]"samaccountname=$($samaccountname)").Findone().Properties.memberof  | ?{$_ -ne $null} | get-group
 $indent = 1
 $master = $groups
 #$Master | get-group | %{write-host $_.name}
 Do {
     $parents = $Groups | %{([ADSISEARCHER]"samaccountname=$($_.SamAccountName)").FindOne().Properties.memberof} | ?{$_ -ne $null} | %{get-group $_ }
     if ($parents -ne $null) {        
         #$Parents| get-group | %{write-host $("`t"*$indent),$_.name}
         $Master += $parents
         $groups = $parents
         $Indent++
     }
 } While ($Parents -ne $null) 
 return $master
}
This does roughly the same as (get-aduser 'eric woodford').AllMemberof  | get-group

AllMemberOf $(Get-Mailbox "Eric Woodford").SamAccountName

Wednesday, June 8, 2016

set-default-outlook-address-book-script

Update June 2016.. 


I have a lot of requests for this script still. Unfortunately, the process that it described was designed for Outlook 2007 (and earlier). It basically had you grab the default profile registry key, then specify the correct hex value for the address book you wanted. Nothing that couldn't be done via simple .REG file import. 

In Outlook 2010 and beyond, Microsoft changed the way the address book is specified. The script would appear to work, but Outlook wouldn't actually pull from the correct value. In my testing, it broke Outlook and I'd need to reset my profile to resolve.. Sigh.

If you'd like to play around with it, I've been able to piece together that post ( with help of cached pages and people's reposting the content in other forums).

To configure the script you'd need to:
  1. Set your Outlook Address Book to the view you want to set on the remote workstation. (Tools - Address Book - Tools - Options - Set show this address list first).
  2. 2.Export the registry key for this Outlook profile.
  3. [HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles\(your default Outlook profile)\9207f3e0a3b11019908b08002b2a56c2]
  4. Open the .REG file and copy the HEX code for "01023d06". For example: 00,00,00,00,dc,a7,40,c8,c0,42,10,1a,b4,b9,08,00,2b,2f,e1,82,01
  5. Edit the Set-DefaultABView.VBS file (see attached) line to use your hex code. Make sure to leave in the double-quotes. Const DestABUsers = "00,00,00,00,dc,a7,40,c8,c0,42,10,1a,b4,b9,08,00,2b,2f,e1,82,01"
'==========================================================================
'
' NAME: Set-DefaultABView.VBS
'  
' Author: Eric Woodford  scripts@ericwoodford.com
' DATE  : 1/28/2009
'
' COMMENT: Set's the default addressbook view in Outlook 2003 and 2007.
'==========================================================================

' this value pulled from registry dump of the registry key "01023d06"
' HKCU\Software\Microsoft\WIndows NT\CurrentVersion\Windows Messaging Subystem\Profiles\My Profile\9207...5C2\

Const DestABUsers = "00,00,00,00,dc,a7,40,c8,c0,42,10,1a,b4,b9,08,00,2b,2f,e1,82,01,00,00,00,00,01,00,00,2f,67,75,69,64,3d,36,34,39,39,36,38,30,31,35,41,36,42,34,43,34,33,39,31,39,39,43,38,45,37,35,46,42,44,37,36,31,33,00"

' ################################### Helper Functions ##################################

Function ArrayAdd (Array, Value)
' add elements to an array
'   Array - array 
'   Value - value to add to array
  Dim ArrayUpper
  If Ubound(Array) < 0 Then
    ReDim Preserve Array(0)
    ArrayUpper = 0
  Else
    ArrayUpper = Ubound(Array) + 1
    ReDim Preserve Array(ArrayUpper)
  End If
  Array(ArrayUpper) = Value
End Function

Function RegistryRead (pServer, pKeyType, pSubTree, pKeyPath, pKeyName, pKeyData, pKeyDataType)
' obtain value of a registry key
'   pKeyData     - data value for that key
'   pkeyDataType - data type for that data value for that key
'   pKeyName     - key name
'   pKeyPath     - path where the key exist
'   pKeyType     - type of key (0 - everytype, 1 - string, 2 - binary, 3 - dword, 
'                  4 - multi-string, 5 - expandable string, 
'                  6 - enumerate multi-keys/folders)
'   pServer      - server name (ex. fg206, if server name is "." then it will use the current system)
'   pSubTree     - registry subtree (0 - HKEY_CLASSES_ROOT, 1 - HKEY_CURRENT_USER, 2 - HKEY_LOCAL_MACHINE,
'                                    3 - HKEY_USERS, 5 - HKEY_CURRENT_CONFIG)

  Const HKEY_CLASSES_ROOT = &H80000000, _
        HKEY_CURRENT_USER = &H80000001, _
        HKEY_LOCAL_MACHINE = &H80000002, _
        HKEY_USERS = &H80000003, _
        HKEY_CURRENT_CONFIG = &H80000005

  Dim objReg
  Dim Result, Server, SubTree
  Result = 0
  Server = pServer
  Set objReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & Server & "\root\default:StdRegProv")
  Select Case pSubTree
    Case 0
      Subtree = HKEY_CLASSES_ROOT
    Case 1
      Subtree = HKEY_CURRENT_USER
    Case 2
      Subtree = HKEY_LOCAL_MACHINE
    Case 3
      Subtree = HKEY_USERS
    Case 5
      Subtree = HKEY_CURRENT_CONFIG
  End Select
  ' define subtree

  Select Case pKeyType
    Case 0
      Result = objReg.EnumValues(SubTree, pKeyPath & "\" & pKeyName, pKeyData, pKeyDataType)
      ' Keydata returns a single dimension array of key names while KeyDataType returns a
      ' a single dimension array of key types
    Case 1
      Result = objReg.GetStringValue(SubTree, pKeyPath, pKeyName, pKeyData)
    Case 2
      Result = objReg.GetBinaryValue(SubTree, pKeyPath, pKeyName, pKeyData)
      ' Keydata returns a single dimension array 
    Case 3
      Result = objReg.GetDWordValue(SubTree, pKeyPath, pKeyName, pKeyData)
    Case 4
      Result = objReg.GetMultiStringValue(SubTree, pKeyPath, pKeyName, pKeyData)
      ' Keydata returns an Array
    Case 5
      Result = objReg.GetExpandableStringValue(SubTree, pKeyPath, pKeyName, pKeyData)
    Case 6
      Result = objReg.EnumKey(SubTree, pKeyPath & "\" & pKeyName, pKeyData)
      ' Keydata returns an object Array
  End Select
  ' obtain key name data depending on key type
  ' objReg.GetStringValue - read a key in string format
  ' objReg.GetBinaryValue - read a key in binary format
  '   process single dimensional array ex: For Cnt = Lbound(Array) to Ubound(Array)
  ' objReg.GetDWordValue - read a key in dword format
  ' objReg.MutliStringValue - read a key in multi-string format/array format
  '   process mutlistring/array ex.: For Each Value in Array
  ' objReg.ExpandableStringValue - read a key in expandable string format
  ' objReg.EnumKey - read a key in an array format 
  '   process array ex.: For Each Value in Array

  RegistryRead = (Result = 0)
  ' pass value out of function, Result should be zero if registry key exist
End Function
' RegistryRead

Function RegistryWrite (pServer, pKeyType, pSubTree, pKeyPath, pKeyName, pKeyData)
' writes a value to a registry key
'   pKeyData     - data value for that key
'   pKeyName     - key name
'   pKeyPath     - path where the key exist
'   pKeyType     - type of key (1 - string, 2 - binary, 3 - dword, 4 - multi-string)
'   pServer      - server name (ex. fg206, if server name is "." then it will use the current system)
'   pSubTree     - registry subtree (0 - HKEY_CLASSES_ROOT, 1 - HKEY_CURRENT_USER, 2 - HKEY_LOCAL_MACHINE,
'                                    3 - HKEY_USERS, 5 - HKEY_CURRENT_CONFIG)

  Const HKEY_CLASSES_ROOT = &H80000000, _
        HKEY_CURRENT_USER = &H80000001, _
        HKEY_LOCAL_MACHINE = &H80000002, _
        HKEY_USERS = &H80000003, _
        HKEY_CURRENT_CONFIG = &H80000005
  ' declare constants

  Dim objReg
  ' declare objects variables

  Dim Result, Server, SubTree
  ' declare variables


  Result = 0
  ' initialize variables

  Server = pServer
  ' define what server to obtain registry info (replace period with server name)

  Set objReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & Server & "\root\default:StdRegProv")
  ' create registry object

  Select Case pSubTree
    Case 0
      Subtree = HKEY_CLASSES_ROOT
    Case 1
      Subtree = HKEY_CURRENT_USER
    Case 2
      Subtree = HKEY_LOCAL_MACHINE
    Case 3
      Subtree = HKEY_USERS
    Case 5
      Subtree = HKEY_CURRENT_CONFIG
  End Select
  ' define subtree

  Select Case pKeyType
    Case 1
      Result = objReg.SetStringValue(SubTree, pKeyPath, pKeyName, pKeyData)
    Case 2
      Result = objReg.SetBinaryValue(SubTree, pKeyPath, pKeyName, pKeyData)
      ' Keydata must be a single dimension array 
    Case 3
      Result = objReg.SetDWordValue(SubTree, pKeyPath, pKeyName, pKeyData)
    Case 4
      Result = objReg.SetMultiStringValue(SubTree, pKeyPath, pKeyName, pKeyData)
      ' Keydata must be an Array
  End Select
  ' set key name data depending on key type
  ' objReg.SetStringValue - write a key in string format
  ' objReg.SetBinaryValue - write a key in binary format (must be a single dimensional array)
  ' objReg.SetDWordValue - read a key in dword format
  ' objReg.SetMutliStringValue - set a key in multi-string format (must be a single dimensional array)

  RegistryWrite = (Result = 0)
  ' pass value out of function, Result should be zero if registry write processed correctly
End Function
' RegistryWrite


Dim NewAddrBook()
ReDim NewAddrBook(-1)
For Each BinValue In Split(DestABUsers,",")
 intv = CInt("&h"&binValue)
 ArrayAdd NewAddrBook, intv
Next


Const OutlookDefaultAddressKey = "9207f3e0a3b11019908b08002b2a56c2", _
      OutlookDefaultAddressSubKey = "01023d06", _
      OutlookLookupAddressSubKey = "11023d05", _
      OutlookProfileLocation = "Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem", _
      OutlookProfileLocationSubKey = "Profiles"
   

OK = RegistryRead (".",1,1,OutlookProfileLocation&"\"&OutlookProfileLocationSubKey,"DefaultProfile",DefaultOKProfile,"")
If OK Then
 KeyLocation = trim(OutlookProfileLocation & "\" & OutlookProfileLocationSubKey & "\" & DefaultOKProfile)
 Ok = RegistryWrite(".", 2, 1, KeyLocation & "\" & OutlookDefaultAddressKey, OutlookDefaultAddressSubKey, NewAddrBook) 
End If



' Create Log file in root drive of current workstation with date and time script ran. 
Const ForReading = 1, ForWriting = 2, ForAppending = 8 
Set objShell = CreateObject("WScript.Shell")
Set colEnvironment = objShell.Environment("PROCESS")
objPath = colEnvironment("username")
Dim strOutFileName: strOutFileName = "C:\"&objpath&".log"
Set fso = CreateObject ("scripting.filesystemobject")
If fso.FileExists(strOutFileName) Then  
 Set Outfile = fso.OpenTextFile(strOutFileName ,ForAppending)
Else
 Set Outfile = fso.OpenTextFile(strOutFileName ,ForWriting, true)
End If
Outfile.writeline Date() & " " & Time()
outfile.close

Wednesday, May 18, 2016

Rapid Math Tricks - Mathematical Curiosity #5

Finally digging out an old book out of the closet. I found this little math curiosity and decided to explore it further. 

12² = 144 while 21² = 441
13² = 169 while 31² = 961
112² = 12,544 while 211² = 44,521
...

function sqr($a) { return $a*$a}
function Rev($b) { return -join([System.Linq.Enumerable]::Reverse($b))}

for($i=0; $i -le 1000;$i++){$ir = [int]$(rev([string]$i));$s = sqr($i);$sir = sqr($ir);$rs = rev([string]$sir);if($rs -eq $s -and $i -ne $ir){write-host $i,$s,$ir,$sir }}

12 144 21 441
13 169 31 961
21 441 12 144
31 961 13 169
102 10404 201 40401
103 10609 301 90601
112 12544 211 44521
113 12769 311 96721
122 14884 221 48841
201 40401 102 10404
211 44521 112 12544
221 48841 122 14884
301 90601 103 10609
311 96721 113 12769

Wednesday, May 11, 2016

Simple Progress Bar

I use the Powershell progress bar (write-progress) quite often. I put it into scripts that have long execution time (like cycling through all 100,000 objects in our GAL. I put it into place when manipulating data from multiple sources. I try to show some progress everywhere things happen that take more than a few seconds.

Problem being, the progress bar requires some setup. You need to know how many objects you are going to touch. You need to maintain an index of what object you are currently on. That I why I started working on these two 'skins' for the progress bar.

My first function uses a global variable that it maintains. This first copy takes the maximum count of items you want to review, then using a the global variable, tracks which item it's currently displaying. [0 .. Item Count]

Function WP {
 [CmdletBinding()] param(
  [Parameter(Mandatory=$true,ValueFromPipeline=$true)][Int]$ArrayItemCount,
  [Parameter()][String]$JobName="Counter"
 )
 $OCount = $ArrayItemCount
 $envVar = get-Variable -Name $JobName -Scope Global -ErrorAction SilentlyContinue -ValueOnly
 if ($EnvVar -eq $null) { 
  #Global Variable doesn't exist, create one called based on $JobName
  $Env_WPIndex = 0
  New-Variable -Name $JobName -Scope Global -Value 0 #-Visibility Private
 } else {
  #Use current global variable value.
  $env_WPIndex = [double]$EnvVar
 }
 #display basic progress bar
 Write-Progress -Activity $JobName -Status $([string]$Env_WPIndex + ":"+[string]$ocount) -PercentComplete (($Env_WPIndex / $oCOunt)*100) 
 $env_WPIndex = $env_wpIndex + 1
 if ($env_wpIndex -lt $OCount) { 
  #if less then max object count, increment the global variable by one
  Set-Variable -Name $JobName -Scope Global -ErrorAction SilentlyContinue -Value $env_WPIndex
 } else {
  #if already greater than max, remove the global variable from machine. 
  Set-Variable -Name $JobName -Scope Global -Value $null # -ErrorAction SilentlyContinue
 }
}



For example:

$Services = get-service
$Services | %{wp $Services.count; write-host $_.name}

The problem I've found with Function WP is sometimes the global variable doesn't get reset at the end of the previous run. This causes your next progress bar to 'wrap around' (started at 11 goes to 100, then back to 11 again). I added the JobName field so that it could spin up a new global for each different iteration if you wish.

The second script is much simpler, but requires you send a copy of the entire array. You pass the function a copy of your array, plus what item you're currently on, and it displays a progress bar based of it's location in the array.

Function WP2 {
 [CmdletBinding()] param(
  [Parameter(Mandatory=$true,ValueFromPipeline=$true)][array]$Array,
  [Parameter(Mandatory=$true,ValueFromPipeline=$true)] $Item
 )
 #Find Index of current item in Array
 $Index = [array]::IndexOf($array, $item)
 #Count items in array
 $ocount = $array.count
 
 Write-Progress -activity "Counter" -Status $([string]$Index + ":"+[string]$ocount) -PercentComplete (($Index/$OCount)*100)
}

For example:

$Services = get-service
$Services | %{wp2 $Services $_ ; write-host $_.name}



Tuesday, January 12, 2016

GAL Cleanup Part 1 - Expire old contacts

Over the 8 years that I've worked here, we've managed to virtually triple the number of contacts we host in our Global Address List. At this point, we have 90,000 mailboxes and 60,000 mail enabled contacts. I suspect that a vast majority of these contacts have not been sent to in several years! Heck, considering the volatile work environment, it's more than likely a good portion of those are no longer good. Customer at another company, leaves and we never delete their contact.

Phase 1:

This got me thinking.. Read in the primary smtp address from 100 or so contacts. Search the message tracking logs (on server hosting Internet bound email) for something going to this contact. Each contact I touch, I would mark Custom Attribute 9, with date/time. If I found an entry in the tracking logs, mark Custom Attribute 10. Relatively easy script to write.

#This is too easy!!
get-contacts -resultsize 100 | ForEach {get-messagetrackinglog -start (7 days ago) -recipient $_.externalemailaddress.addressstring.tostring()}

Unfortunately, that TOOK FOREVER!
  1. We have 12 hub transports that send out email to the Internet. This means to effectively scan for a single recipient, I'd need to scan all 12 servers. 
  2. We process probably a million messages each day going out to the Internet.
  3. As I said, we have 60,000 mail enabled contacts.
  4. We keep tracking logs back 30 days.
If I let it run in this state, we'd be scanning 1/30th contacts every day each month..

Phase 2:

I noticed that the Recipient field on get-messagetrackinglog uses OR logic. I could technically buffer up a big handful of recipients into that field and search for them all at once. 20, I'll start with twenty recipients per search. 

These are not the results I was hoping for...
Evidently, I've stumbled onto a 'known bug' with the cmdlet. Your search has to be under so many characters (256 iirc). Once you exceed that, it fails. Only workaround is to reduce the # of entries in your query. At one point, I reduced my # of recipients to only 5 addresses and the script was failing. What next? Only 2 people at a time? Not much of a time savings. 

Phase 3:

While walking out to my car that night I was discussing this project with a co-worker. During explaining the concept to him, I came up with an interesting idea. Scan message tracking logs for non-mailbox users. OK, it sounds worse, but it pays off. 
  1. Create giant string of every accepted domain. This will be used to filter out every mailbox recipient.
  2. Find and fine-tune 'directory searcher' function to validate email address is in GAL. 
So here's my basic process. On each hub transport:
  •  read message tracking logs and spit out all recipients
  • filter where internal domains -notmatch external email address domain
  • check GAL to see if contact exists for recipient.
  • get mailcontact - put today's date in CA10.
Now some contacts appear to get messages hourly as part of scheduled tasks. So I created a second filter on already touched contacts. 

Exchange 2010 RPC Client Access Logs + Powershell + LogParser

I've been working on a few projects revolving around analyzing Outlook clients connecting to our email environment. The latest request was for a report that would detail how many clients from one office are connecting to email via OWA vs Outlook. While I've seen the function that details CAS connections, it won't work for me. My customers range from over 45 different offices and they include vastly different #s of concurrent users. Many of the offices share the same CAS servers/pools.

This got me looking at the RPC Client Access logs on each box. From here I can filter my connections based on individual mailboxes, members of the same OU, or even distribution group members. Sadly with 40,000 active users, connecting to possible 60+ CAS boxes, that's easily a 9GB of data per person of log data to go through. While I have developed fairly efficient powershell to process this data, (it requires effectively reading in the log file for each server as a CSV and processing it individually) I believe I can do better.

LogParser 2.2

ExchangeServerPro published an article about using LogParser to query RPC Client Access logs. Reviewing this article, I was able to pick up the basics about building a LP query for RPC client. This got me to this: 

#Set up some basics for the script.
#Default path to the RPC Client Access logs. Going to build paths to.
$LogPath = "\C$\Program Files\Exchange\Logging\RPC Client Access\"
#Default path for LogParser executable
$LogparserExec = "C:\Program Files (x86)\Log Parser 2.2\logparser.exe"


#Final Report Path
$TodayString = (Get-Date -Format "yyyyMMdd").tostring()
$ReportPath = "c:\reports\OutlookClientReport_"+$TodayString + ".CSV"

#Get names for all CAS boxes
$CASPool = get-clientaccessServer | %{$_.name}

#Build source statement for all log files. (Reading only today's logs).
[array]$logPaths = $null
$CASPool | %{$logPaths += "'\\"+$_+$LogPath+"RCA_"+$todayString + "*.log'"}
$allServers = $logPaths -join(";")

#Build one REALLY BIG query.
$Query = "SELECT EXTRACT_SUFFIX(client-name,0,'=') as Name,client-software as Software,client-software-version as Version INTO '"+$ReportPath+"' FROM "+$allServers + " where software in ('outlook.exe';'OUTLOOK.EXE') Group BY Name,Software,Version ORDER BY Name"

#Execute LogParser search
& $LogparserExec $Query -i:CSV -nSkipLines:4  # -Stats:Off


When executed, this generates a CSV in the report path specified. It contains each customer who's connected today, and all the Outlook client version they connected with.
Statistics:
-----------
Elements processed: 9140619
Elements output:    53206
Execution time:     135.88 seconds (00:02:15.88)
As the 'Client-Name' field is based off the end-user's mailbox alias and/or legacyExchangeDN, you can search for a specific user based off it.


#Get specific mailbox path
$cn = (Get-Mailbox "End-User, Joe").LegacyExchangeDN

#Query that specifies client we are looking for.
$Query = "SELECT EXTRACT_SUFFIX(client-name,0,'=') as Name,client-software as Software,client-software-version as Version INTO '"+$ReportPath+"' FROM "+$allServers + " where software in ('outlook.exe';'OUTLOOK.EXE') and client-name='"+$cn+"' Group BY Name,Software,Version ORDER BY Name"

Now you are searching all of your client access servers for Outlook connections from this specific user. My average search time for a day's log files is about the same 2 minutes per person. 2,000 people will take 4,000 minutes, or about 2 days to run.