Saturday, November 28, 2009

Basic Powershell script to show appointments from a calendar using the EWS Managed API

One of the more common tasks you might turn to use one of the Exchange API's for is to enumerate appointments in one or more users calendars. This can be useful for a number of reasons and applications and scripts, the EWS Managed API makes getting calendar appointment pretty easy using a script once you understand a few of the fundamentals around how calendar appointment's are stored.

Appointments unlike messages are more complex objects eg where an email would only represent one object in a folder an appointment if its recurring or a recurring exception or a deleted occurrence could represent one more calendar appointments. So its important not just to do a finditems query like you would with Email messages as this will return just the base instances of each appointment object. What you should do when querying for calendar appointments is its important to use a CalendarView, what this means is that when you query for appointments you specify a date range (start and end) for the appointments you what to retrieve and EWS will handle expanding any recurring appointments,deleted occurrences etc and return you what is essentially what you would see in Outlook (which does the same thing). This is a rather simplified explanation if you want to read more check out this sample chapter from Inside Exchange Web Services which is available on MSDN.

So when it comes to putting all this together in Powershell script that will pull all the appointments from a specific calendar we first need to specify the SMTP address of a mailbox you want to access and a Date range you want to check in a few variables

$MailboxName = "gscales@domain.com"
$StartDate = new-object System.DateTime(2009, 08, 01)
$EndDate = new-object System.DateTime(2009, 11, 01)

or maybe you want to look at the appointment for next week

$StartDate = new-object [System.DateTime]::Now
$EndDate = new-object [System.DateTime]::Now.AddDays(7)

Connect to EWS using the currently logged on Credentials the assumes that this account has access to the mailbox is question.

$dllpath = "C:\Program Files\Microsoft\Exchange\Web Services\1.0\Microsoft.Exchange.WebServices.dll"
[void][Reflection.Assembly]::LoadFile($dllpath)
$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2007_SP1)

$windowsIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$sidbind = "LDAP://<SID=" + $windowsIdentity.user.Value.ToString() + ">"
$aceuser = [ADSI]$sidbind

$service.AutodiscoverUrl($aceuser.mail.ToString())

Then you need to specify the calendarview object to use

$folderid = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Calendar,$MailboxName)
$CalendarFolder = [Microsoft.Exchange.WebServices.Data.CalendarFolder]::Bind($service,$folderid)
$cvCalendarview = new-object Microsoft.Exchange.WebServices.Data.CalendarView($StartDate,$EndDate,2000)

and finally query the calendar and return the appointments

$cvCalendarview.PropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
$frCalendarResult = $CalendarFolder.FindAppointments($cvCalendarview)
foreach ($apApointment in $frCalendarResult.Items){
$psPropset = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
$apApointment.load($psPropset)
"Appointment : " + $apApointment.Subject.ToString()
"Start : " + $apApointment.Start.ToString()
"End : " + $apApointment.End.ToString()
"Organizer : " + $apApointment.Organizer.ToString()
"Required Attendees :"
foreach($attendee in $apApointment.RequiredAttendees){
" " + $attendee.Address
}
"Optional Attendees :"
foreach($attendee in $apApointment.OptionalAttendees){
" " + $attendee.Address
}
"Resources :"
foreach($attendee in $apApointment.Resources){
" " + $attendee.Address
}
" "
}

I've put a download of the script here.


Friday, November 13, 2009

Changing what OutlookAnywhere proxy a pc is using remotely via a script

During the course of migrating from one version of Exchange to the another version of Exchange or just moving between servers you might come across the need to shift some of your OultookAnywhere users who are proxying through one CAS server to another without taking the existing CAS server offline and letting autodiscover sort it out. Well i did so here's how you can do it

First of all this script only works if you can connect to the target pc via WMI remotly and access the registry. If this is all good here's what the script needs to do .

First things first is you need to work out the machine name of the PC you want to connect to and the username of the user you want to move. Eg Get-Logonstatitics | select Username,ClientIpAddress should provide that (you may have to use the IISlog files to workout if they are using OutlookAnywhere).

Now we have the hostname orIpAddress of the machine we want to target we now need to work out the SID of the user. Why you ask well where going to be using the Hkey_User key in the registry to make the change so the path to access the users profile setting includes the SID of that user. To work out the SID of the user we take the username and then using ADSI find the AD object and then thanks to a script from Richard Mueller from this ng post we can get the SID from this user object in the format needed to use in the registry path.

Okay great so now we can use WMI to connect to registry Keys that belong to the user we are targeting but which keys do we need to change ? . Thanks to Vinay's script and Oz for his PRF script this helped work out the two entries we are intersted in which are the

RPCProxyServer=PT_UNICODE,0x6622
RPCProxyPrincipalName=PT_UNICODE,0x6625

Now to start glueing this all together I've added some code the first enums all these keys for all profiles for that user and reads those registry entries so the script can specifically target one proxy setting to change (eg only change those values that equal the old proxy server setting) in all profiles. To write the registry entries theres some code the first converts the string you entry for the proxyname into a binary array that can be written to the registry finally this code came out of another script of Sue Mosher's for doing signatures in Outlook.

Okay so to run this script it would look something like this.

cscript changeprx.vbs hostname username oldproxy.com.au newproxy.com.au

I've posted a copy of this script here the script itself looks like

SourceServer = wscript.arguments(2)
TargetServer = wscript.arguments(3)

strComputer = wscript.arguments(0)
strUser = wscript.arguments(1)

set conn = createobject("ADODB.Connection")
set com = createobject("ADODB.Command")
Set iAdRootDSE = GetObject("LDAP://RootDSE")
strNameingContext = iAdRootDSE.Get("defaultNamingContext")
Conn.Provider = "ADsDSOObject"
Conn.Open "ADs Provider"
mbQuery = "<LDAP://" & strNameingContext & ">;(&(objectclass=person)(samaccountname=" & strUser & "));name,distinguishedName;subtree"
Com.ActiveConnection = Conn
Com.CommandText = mbQuery
Set Rs = Com.Execute
While Not Rs.EOF
Userdn = rs.fields("distinguishedName")
wscript.echo Userdn
rs.movenext
Wend
Set objUser = GetObject("LDAP://" & Userdn)
usrsid = ObjSidToStrSid(objUser.objectSid)

wscript.echo "UserSid : " & usrsid

Const HKEY_USERS = &H80000003


Set oReg =GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & _
strComputer & "\root\default:StdRegProv")

prProxyVal = "001f6622"
crcertVal = "001f6625"

strKeyPath = usrsid & "\Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles"

oReg.EnumKey HKEY_USERS, strKeyPath, arrSubKeys

For Each subkey In arrSubKeys
strFullPath = strKeyPath & "\" & subkey & "\13dbb0c8aa05101a9bb000aa002fc45a"
oReg.GetBinaryValue HKEY_USERS,strFullPath,prProxyVal,prProxyValRes
sval = ""
if not IsNull(prProxyValRes) then
For Each byteValue in prProxyValRes
sval = sval & ChrB(byteValue)
Next
if mid(sval,1,len(SourceServer)) = SourceServer then
newarrValue = StringToByteArray(TargetServer, True)
newarrValue2 = StringToByteArray(("msstd:" & TargetServer), True)
oReg.SetBinaryValue HKEY_USERS,strFullPath,prProxyVal,newarrValue
oReg.SetBinaryValue HKEY_USERS,strFullPath,crcertVal,newarrValue2
wscript.echo "Changed : " & sval
end if
end if

Next

Public Function StringToByteArray _
(Data, NeedNullTerminator)
Dim strAll
strAll = StringToHex4(Data)
If NeedNullTerminator Then
strAll = strAll & "0000"
End If
intLen = Len(strAll) \ 2
ReDim arr(intLen - 1)
For i = 1 To Len(strAll) \ 2
arr(i - 1) = CByte _
("&H" & Mid(strAll, (2 * i) - 1, 2))
Next
StringToByteArray = arr
End Function


Public Function StringToHex4(Data)
' Input: normal text
' Output: four-character string for each character,
' e.g. "3204" for lower-case Russian B,
' "6500" for ASCII e
' Output: correct characters
' needs to reverse order of bytes from 0432
Dim strAll
For i = 1 To Len(Data)
' get the four-character hex for each character
strChar = Mid(Data, i, 1)
strTemp = Right("00" & Hex(AscW(strChar)), 4)
strAll = strAll & Right(strTemp, 2) & Left(strTemp, 2)
Next
StringToHex4 = strAll
End Function

Function ArrayToMB(A)
Dim I, MB
For I = LBound(A) To UBound(A)
MB = MB & ChrB(A(I))
Next
ArrayToMB = MB
End Function


Function ObjSidToStrSid(arrSid)
' Function to convert OctetString (byte array) to Decimal string (SDDL)
Dim strHex, strDec

strHex = OctetStrToHexStr(arrSid)
strDec = HexStrToDecStr(strHex)
ObjSidToStrSid = strDec
End Function ' ObjSidToStrSid

Function OctetStrToHexStr(arrbytOctet)
' Function to convert OctetString (byte array) to Hex string.
Dim k

OctetStrToHexStr = ""
For k = 1 To Lenb(arrbytOctet)
OctetStrToHexStr = OctetStrToHexStr _
& Right("0" & Hex(Ascb(Midb(arrbytOctet, k, 1))), 2)
Next
End Function ' OctetStrToHexStr

Function HexStrToDecStr(strSid)
Const BYTES_IN_32BITS = 4
Const SRL_BYTE = 0
Const IAV_START_BYTE = 2
Const IAV_END_BYTE = 7
Const RID_START_BYTE = 8
Const MSB = 3 'Most significant byte
Const LSB = 0 'Least significant byte

Dim arrbytSid, lngTemp, base, offset, i

ReDim arrbytSid(Len(strSid)/2 - 1)

' Convert hex string into integer array
For i = 0 To UBound(arrbytSid)
arrbytSid(i) = CInt("&H" & Mid(strSid, 2 * i + 1, 2))
Next

' Add SRL number
HexStrToDecStr = "S-" & arrbytSid(SRL_BYTE)

' Add Identifier Authority Value
lngTemp = 0
For i = IAV_START_BYTE To IAV_END_BYTE
lngTemp = lngTemp * 256 + arrbytSid(i)
Next
HexStrToDecStr = HexStrToDecStr & "-" & CStr(lngTemp)
For base = RID_START_BYTE To UBound(arrbytSid) Step BYTES_IN_32BITS
lngTemp = 0
For offset = MSB to LSB Step -1
lngTemp = lngTemp * 256 + arrbytSid(base + offset)
Next
HexStrToDecStr = HexStrToDecStr & "-" & CStr(lngTemp)
Next

End Function ' HexStrToDecStr

Saturday, November 07, 2009

Reporting on Outlook Anywhere users from the IIS logs using Powershell

Logs files are great things but as with a lot of things the hidden value of these resources is often hard to see at there face value. I had a very simple problem this week where i needed to know which users where using Outlook Anywhere and what their IPAddress where. For this the IIS logs could more then provide an answer for this but these log files and get quite large and contain a mix of different information not just Outlook http/rpc traffic.

One of the main challenges you have when looking at IIS logs files is that depending on who enabled logging the Fields and the order they are being logged in may differ. So a parser/script you write to work against one set of logs might not work against another server. To solve this problem is quite simple when a log file is created at the top of the log file on line 4 is written the field format for the file. So if you first read this line parse it into a Hashtable and index it by the fieldname we then know what the index array value will be when you go to read each line of the log file for the field you want to extract.

To make the script easy to use I've added a front end GUI component to make the script present a file select box to select the log file to work with. It then reads the file using get-content and filters it based on OutlookAnywhere log entries using a like filter on “*MSRPC*”. The script groups the results by IPAddress and UserName and builds a exportable collection that produces a csv file or the results of the scan with a simple list of OutlookAnywhere users and what IP address they came from. The cool thing about the script is that it can be easily adapted to scan for anything in a IIS log file eg say you wanted a list of people using a Iphone and their IPaddress.

I've put a download of the script here the script itself looks like

[System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")
$exFileName = new-object System.Windows.Forms.openFileDialog
$exFileName.ShowHelp = $true
$exFileName.ShowDialog()

$fname = $exFileName.FileName
$mbcombCollection = @()
$FldHash = @{}
$usHash = @{}
$fieldsline = (Get-Content $fname)[3]
$fldarray = $fieldsline.Split(" ")
$fnum = -1
foreach ($fld in $fldarray){
$FldHash.add($fld,$fnum)
$fnum++
}

get-content $fname | Where-Object -FilterScript { $_ -ilike “*MSRPC*” } | %{
$lnum ++
if ($lnum -eq $rnma){ Write-Progress -Activity "Read Lines" -Status $lnum
$rnma = $rnma + 1000
}
$linarr = $_.split(" ")
$uid = $linarr[$FldHash["cs-username"]] + $linarr[$FldHash["c-ip"]]
if ($linarr[$FldHash["cs-username"]].length -gt 2){
if ($usHash.Containskey($uid) -eq $false){
$usrobj = "" | select UserName,IpAddress
$usrobj.UserName = $linarr[$FldHash["cs-username"]]
$usrobj.IpAddress = $linarr[$FldHash["c-ip"]]
$usHash.add($uid,$usrobj)
$mbcombCollection += $usrobj

}
}
}

$mbcombCollection | export-csv –encoding "unicode" -noTypeInformation c:\oareport.csv