Friday, February 27, 2009

Twitter a Exchange Calendar using Powershell and EWS - Exchange 2007

Micro-Blogging is one of the Web 2.0 technologies that has been getting a bit of run of late its one of the tools a seemingly new breed of politicians are using to garner support and awareness of their movements. But its also used by a growing number of everyday people and businesses as a method of pervaying certain messages and other sometimes seemly useless pieces of information. Maybe one of the keys to twitter's success is it fills the gap between availibilty and straight calendaring. I think the scope of what could be done with twitter when you start to intergrate this into traditional communication technology should see this technology expand even more (well as long as we get some creative people pushing the limits).

Exchange mostly provides the same type of information in different formats so using the Twitter API and the Exchange API's you can start to combine the functionality of both systems to start providing different new funcationality. Eg in this sample Im using a script to query an Exchange calendar and then take the text from the current appointment (if there is one) trim it to 140 characters and then post this as a Twitter update. There's logic in the script to first check the current twitter status so the script can be run at intervals and ensure that multiple updates wont be sent for multiple appointments. If you have more then one appointment scheduled at the same time it will just use the first.

So what could you use it for ?.. well any compay,club or small group who has a calendar or events can create twitter feed for these event by simply placeing the events in a Exchange calendar and then let the script read and twitter the status updates. Most of our lives are scripted by our calendar anyway so here a way to save on typing.

How does it work.

There are already a few good Powershell twitter scripts so i grabbed some code out of Mike Ormonds's Sample and started working in some other code. The first thing the scirpt does is goes and grabs the current twitter status. It then goes and queries the mailbox calendar to see if there are any appointments that are in the current time frame. If it find one it then posts the subject of the appointment as a twitter status update. To do the Exchange side I've use Exchange Web Services via some code i've packed into my EWSUtil powershell library (actually im reusing the stuff from the RSS feed script).

Making it work

To make this work you first need a twitter account and a Exchange 2007 Mailbox you need to put your twitter username and password in the following varibles

$twitterusername = "username"
$twitterpassword = "password"

Then put the Email address of the mailbox with the calendar you want to pull the data from and the Exchange Username and password to access this mailbox (or you can use EWS impersonation if you wanted to agregate several mailboxes) in the following varibles.

$emEmailAddress = "twittest@domain.com"
$userName = "twittest"
$password = "Password"
$domain = "domain"

In the

$ExchangeServername = "servername"

put the CAS Servername

If your using a cloud mailbox on Microsoftonline set the domain to blank and use something like (I've built this using a BPOS mailbox)

$emEmailAddress = "twittest@youdomain.microsoftonline.com"
$userName = "twittest@youdomain.microsoftonline.com"
$password = "youpassword"
$domain = ""

To use this you need the EWSUtil library I've put a download of this script here the script itself looks like (note this script is pretty alpha issh so do your own testing first)

$twitterusername = "username"
$twitterpassword = "password"
$ExchangeServername = "servername"
$emEmailAddress = "twittest@domain.com"
$userName = "twittest"
$password = "Password"
$domain = "domain"

function updateTwiterStatus($PostString){

$tweetstring = [String]::Format("status={0}", $PostString )
[System.Net.ServicePointManager]::Expect100Continue = $false
$request = [System.Net.WebRequest]::Create("http://twitter.com/statuses/update.xml")
$request.Credentials = new-object System.Net.NetworkCredential($twitterusername,$twitterpassword)
$request.Method = "POST"
$request.ContentType = "application/x-www-form-urlencoded"
$formdata = [System.Text.Encoding]::UTF8.GetBytes($tweetstring)
$request.ContentLength = $formdata.Length
$requestStream = $request.GetRequestStream()
$requestStream.Write($formdata, 0, $formdata.Length)
$requestStream.Close()
$response = $request.GetResponse()
$reader = new-object System.IO.StreamReader($response.GetResponseStream())
$returnvalue = $reader.ReadToEnd()
$reader.Close()

}

function GetTwitterStatus(){
[System.Net.ServicePointManager]::Expect100Continue = $false
$request = [System.Net.WebRequest]::Create("http://twitter.com/users/show/" + $twitterusername)
$request.Credentials = new-object System.Net.NetworkCredential($twitterusername,$twitterpassword)
$request.Method = "GET"
$request.ContentType = "application/x-www-form-urlencoded"
$response = $request.GetResponse()
$ResponseStream = $response.GetResponseStream()
$ResponseXmlDoc = new-object System.Xml.XmlDocument
$ResponseXmlDoc.Load($ResponseStream)
$StatusNodes = @($ResponseXmlDoc.getElementsByTagName("status"))
$returnStatus = $StatusNodes[0].text
$ResponseXmlDoc.Save("c:\dd.xml")
return $returnStatus.ToString()
}


$casURL = "https://" + $ExchangeServername + "/EWS/Exchange.asmx"
[void][Reflection.Assembly]::LoadFile("C:\temp\EWSUtil.dll")
$ewc = new-object EWSUtil.EWSConnection($emEmailAddress,$false, $userName, $password,$domain, $casURL)
$drDuration = new-object EWSUtil.EWS.Duration
$drDuration.StartTime = [DateTime]::UtcNow
$drDuration.EndTime = [DateTime]::UtcNow.AddMinutes(15)
$upset = 0
[EWSUtil.EWS.DistinguishedFolderIdType] $dType = new-object EWSUtil.EWS.DistinguishedFolderIdType
$dType.Id = [EWSUtil.EWS.DistinguishedFolderIdNameType]::calendar
$qfolder = new-object EWSUtil.QueryFolder($ewc,$dType, $drDuration,$false,$true)
foreach ($item in $qfolder.fiFolderItems){
if ($item.Location -ne $null){
$twitString = $item.Subject.ToString() + " " + $item.Location.ToString()
}
else{
$twitString = $item.Subject.ToString()
}
if ($twitString.Length -gt 140){$twitString = $twitString.Substring(0,139)}
[String]$currentStatus = GetTwitterStatus
$twitString = $twitString.Substring(0,$twitString.length-1)
write-host $twitString.ToLower().ToString()
write-host $currentStatus.ToLower().ToString()
if ($currentStatus.ToLower().ToString() -ne $twitString.ToLower().ToString()){
if ($statusup -ne 3){$statusup = 1}
$upset = 1
}
else{
$statusup = 3
}
}
if ($statusup -eq 1){
updateTwiterStatus($twitString)
"Twitter Status Updated"
}
else {"Nothing changed since last update"}

Sunday, February 22, 2009

Content Filtering System Whitelist GUI for Exchange 2007

If you’re using the built-in Content Filtering in Exchange 2007 on a Hub or Edge server you may want to take advantage of System white lists. But if you have used the Set-ContentFilterSetting cmdlet you may have noticed this isn't the most user friendly cmdlet. If you’re maintaining these lists on a regular basis you may really start to dislike the lack of a GUI well I certainly do so I built a pretty simple GUI to make managing the BypassedSenders and BypassedSenderDomains a more enjoyable experience.

It’s pretty simple code first it uses Get-ContentFilterSetting to get the current setting of both of these properties and then displays then in a DatagridView. You can then add more entries or change existing entries by editing the flexGrid then when you hit the update button it will run back through the datagrid values and then creates a collection which is then used to update the ByPassedSenders or ByPassedSenderDomains properties.

I've put a download of the script here the script look like

[System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")

function UpdateBypassSenders(){
$newcombCollection = @()
foreach($Item in $dgDataGrid.Rows){
if ($item.Cells[0].Value -ne $null){
if ($item.Cells[0].Value -ne ""){
$newcombCollection += $item.Cells[0].Value
}
}
}
Set-ContentFilterConfig -BypassedSenders $newcombCollection
$fsTable.Clear()
$contentFilterConfig = get-ContentFilterConfig
foreach ($ent in $contentFilterConfig.BypassedSenders){
$fsTable.Rows.add($ent.ToString())
}
$dgDataGrid.DataSource = $fsTable
}

function UpdateBypassSenderDomains(){
$newcombCollection = @()
foreach($Item in $dgDataGrid1.Rows){
if ($item.Cells[0].Value -ne $null){
if ($item.Cells[0].Value -ne ""){
$newcombCollection += $item.Cells[0].Value
}
}
}
Set-ContentFilterConfig -BypassedSenderDomains $newcombCollection
$fsTable1.Clear()
$contentFilterConfig = get-ContentFilterConfig
foreach ($ent in $contentFilterConfig.BypassedSenderDomains){
$fsTable1.Rows.add($ent.ToString())
}
$dgDataGrid1.DataSource = $fsTable1
}

$form = new-object System.Windows.Forms.form

$contentFilterConfig = get-ContentFilterConfig
$Dataset = New-Object System.Data.DataSet
$fsTable = New-Object System.Data.DataTable
$fsTable.TableName = "BypasssedSender"
$fsTable.Columns.Add("WhitelistEntry")
$BypassSenderArray = $contentFilterConfig.BypassedSenders.ToString().Split(',')

foreach ($ent in $contentFilterConfig.BypassedSenders){
$fsTable.Rows.add($ent.ToString())
}

$fsTable1 = New-Object System.Data.DataTable
$fsTable1.TableName = "BypassedSenderDomains"
$fsTable1.Columns.Add("WhitelistEntry")

foreach ($ent in $contentFilterConfig.BypassedSenderDomains){
$fsTable1.Rows.add($ent.ToString())
}

$dgDataGrid = new-object System.windows.forms.DataGridView
$dgDataGrid.Location = new-object System.Drawing.Size(20,50)
$dgDataGrid.size = new-object System.Drawing.Size(250,450)
$dgDataGrid.AutoSizeRowsMode = "AllHeaders"
$form.Controls.Add($dgDataGrid)
$dgDataGrid.DataSource = $fsTable

$dgDataGrid1 = new-object System.windows.forms.DataGridView
$dgDataGrid1.Location = new-object System.Drawing.Size(320,50)
$dgDataGrid1.size = new-object System.Drawing.Size(250,450)
$dgDataGrid1.AutoSizeRowsMode = "AllHeaders"
$form.Controls.Add($dgDataGrid1)
$dgDataGrid1.DataSource = $fsTable1




# Add Update Button

$exButton = new-object System.Windows.Forms.Button
$exButton.Location = new-object System.Drawing.Size(20,530)
$exButton.Size = new-object System.Drawing.Size(150,20)
$exButton.Text = "Update - Senders"
$exButton.Add_Click({UpdateBypassSenders})
$form.Controls.Add($exButton)

# Add Update Button

$exButton1 = new-object System.Windows.Forms.Button
$exButton1.Location = new-object System.Drawing.Size(320,530)
$exButton1.Size = new-object System.Drawing.Size(150,20)
$exButton1.Text = "Update - Domains"
$exButton1.Add_Click({UpdateBypassSenderDomains})
$form.Controls.Add($exButton1)

$Gbox = new-object System.Windows.Forms.GroupBox
$Gbox.Location = new-object System.Drawing.Size(10,15)
$Gbox.Size = new-object System.Drawing.Size(270,550)
$Gbox.Text = "By-Passed Senders"
$form.Controls.Add($Gbox)

$Gbox1 = new-object System.Windows.Forms.GroupBox
$Gbox1.Location = new-object System.Drawing.Size(310,15)
$Gbox1.Size = new-object System.Drawing.Size(270,550)
$Gbox1.Text = "By-Passed Sender Domains"
$form.Controls.Add($Gbox1)

$form.Text = "Exchange 2007 WhiteList Form"
$form.size = new-object System.Drawing.Size(600,600)
$form.autoscroll = $true
$form.Add_Shown({$form.Activate()})
$form.ShowDialog()

Thursday, February 12, 2009

Adding a Public Contact Folder Search to the OWA 2007 Address book

Public folders are a great thing they increase the functionality of an Exchange server and while they can cause some support issues it’s great to see they will still be included in the next version of Exchange (it would be great to see them embraced as a feature instead of a burden as they provide some unique functionality you don’t get on other mail systems). One thing that Public folders can be used for is shared address lists with Outlook supporting the ability to add a Public Folder as an address list location in the Outlook Address Book. OWA has never provided this same functionality but for 2003 I came up with a small unsupported (hack) customization to add a public folder search option to the find address function see this post. On 2007 OWA was rewritten and WebDAV is no longer used by any of the OWA code but using the same underlying methods a similar public folder search option can be made available in 2007. Like before this involves modifying some of the OWA files which is not supported due to the nature of these files changing when Service packs and rollup/hotfixes are installed. Although you can change the template forms in 2007 there is still no way of controlling the way things get rendered so this method still uses a HTML injection via the document model. There are several places this code could be placed in 2003 I used the script files which would still work in 2007 but if you have looked at the script files in 2007 you will see a lot more code has been crammed into them and they seem to have gone on a diet in terms of the verbosity of naming standards for variables and whitespace I guess this was done to improve performance. So I chose to modify the addressbook.aspx template form which makes this mod a little easier to service.

How does it work?

The mod involves placing a script block of code just before the closing body tag the reason for positioning it down here is that we want this code to run after the page elements have been rendered to aid with this the defer tag is also used which should defer running of the script until the body has loaded. This could have also been done by using the Onload event in the body but this is already used by the other script files so overloading it in the template breaks other things and I wanted to avoid modifying the scripts themselves. I found the method I described above in my limited testing works okay. What this script block does is gets an array of Divs in the body and then searches through them for a div with an id of divEnts. The first div it finds will represent the upper Address Book part of this page so we don’t want to modify this. The second div it finds with this Id should represent the Contacts section which is where we want to inject the html. So what this script does is inject another entry into this list by using the Innerhtml property of the existing divEnts div. The Div that is injected contains the OWA public folderID of the public folder you want to search as well as the displayName of what you want it to appear as. These two parts need to be customized by you if you wish to use this method. To find the OWAID of the public folder you could use a tool like fiddler to look at protocol captures of OWA or I have another method that uses the EWS convertID operation to convert the Hex entry ID which can be easily obtained using the standard EMS Get-PublicFolder cmdlet.

Finding the OWAID for the Public Folder

Using the standard EMS cmdlet Get-PublicFolder you get access the EntryID of the public folder in its Hex form eg

$pfPublicFolderPath = "\Public Contacts"
$pfFolder = get-publicFolder -identity $pfPublicFolderPath

This would get the Public Folder called Public Contacts in the root and then you can access the EntryID from the EntryID property eg $pfFolder.EntryID. Once you have this EntryID you then need to convert it to the OWAID so it can be used in OWA to do this you need to use the Exchange Web Service convertid operation. I’ve added the necessary EWS code to my EWSUtil powershell library to do this so the code you need just to do the conversion looks like.

[void][Reflection.Assembly]::LoadFile("C:\temp\EWSUtil.dll")
$null = [Reflection.Assembly]::LoadWithPartialName("System.Web")
$mbMailboxEmail = "user@domain.com”
$ewc = new-object EWSUtil.EWSConnection($mbMailboxEmail,$false, "", "", "","")
$Oulookid = $ewc.convertHexidPublicFolder($pfFolder.Entryid,[EWSUtil.EWS.IdFormatType]::OWAid)
[System.Web.HttpUtility]::UrlDecode($Oulookid)

You need to add the email address of the logged on user to the varible $mbMailboxEmail.When you run this code you should see an ID like

PSF.LgAAAAAaRHOQqmYRzZvIAKoAL8RaAwACQRpk/O1eTKnFP2PCsr25AAXCkNnpAAAB

Outputted to the commandline you need to copy this ID for use in the OWA addressbook form.

Modifying the Addressbook.aspx file

Before making any changes to the addressbook.aspx file make sure you copy this file to another location so you have a backup of this file incase the modification doesn’t work and you need to roll back the changes.

On the CAS server locate the OWA forms directory and then locate the addressbook.aspx file in the premium directory see



Open this file in Notepad.
Down the bottom of the file the last three entries in this file should look something like

<% RenderEndOfFileDiv(); %>
</body>
</html>

What we are going to do is put some script in just before the end body tag so the modified end of file should look like

<% RenderEndOfFileDiv(); %>

<script type="text/javascript" language="JavaScript" defer>
var dc=0
var divCollection = document.getElementsByTagName("div");
for (var i=0; i<divCollection.length; i++) {
if(divCollection[i].getAttribute("id") == "divEnts") {
if(dc == 1){
var pfId = "PSF.LgAAAAAaRHOQqmYRzZvIAKoAL8RaAwACQRpk/O1eTKnFP2PCsr25AAXCkNnpAAAB"
var pfName = "Company Contacts"
var newContactEntry = "<div class=snlEntW><div id=divEnt class=\"snlEnt snlDef\" _onclick=onClkCntFld() _fid=\"" + pfId +"\" type=\"IPF.Contact\"><img src=\"current/themes/base/cntctsmll.gif\"><span id=spn>" + pfName + "</span></div></div>";
divCollection[i].innerHTML = divCollection[i].innerHTML + newContactEntry
}
dc++;
}
}
</script>

</body>
</html>


The two varibles

var pfId = "PSF.LgAAAAAaRHOQqmYRzZvIAKoAL8RaAwACQRpk/O1eTKnFP2PCsr25AAXCkNnpAAAB"
var pfName = "Company Contacts"


need to be set to the OWAID you retrieved before and the name of the folder you want.

Thats it the change should be pretty much live as soon as you commit the changes . You need to consider this an untested and unsupported method and only for your own experimentation and testing under lab conditions (make sure your lab has SP1 installed or this wont work). I've put a download of the code to retrieve the OWA public folder ID and the other script change here. For the OWAid code you will need the latest copy of my EWSUtil powershell library