Saturday, August 29, 2009

Connections Online Exchange Powershell Talk

For those of you who have ever wished to see what it takes to build some of the more advanced scripts that i post on this blog as part of the Connections Online conference I put together the following talk http://videos.devconnections.com/product/Exchange-Management-Shell-Beyond-the-One-Liner,5789-3.aspx . While i cant promise that this will teach you to be a guru in 60 minutes hopefully it may present a few different idea's and methods you may not have considered before and allow you to bash out a few more of your own scripts. There are also a lot of great videos on various aspects of Exchange posted by other Exchange MVP's that are defiantly worth checking out http://videos.devconnections.com/catalog/Exchange,390.aspx

Changes to SearchFilters in the EWS Managed API RC

With the recent release of the RC for the EWS Managed API there was one change that affected a lot of the scripts i've posted on this blog. While i will update them all in time if you have created any scripts of your own based on these you will need to look at making the following changes to allows your scripts to function correctly when you update to the RC of the Managed API.

The change was

  • In a FindItems call, the search filter now has to be passed as a parameter to FindItems, as opposed to being a property of ItemView
So to put that in a powershell sample where before the search filter was a property of the view object you now need to create a seperate searchfilter object and the pass this as a parameter of the finditems overload. eg

$Sf = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::IsRead, $false)
$view = New-Object Microsoft.Exchange.WebServices.Data.ItemView(1)
$findResults = $service.FindItems([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$Sf,$view)

or if your binding to the folder directly eg

$inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox)
$findResults = $inbox.Finditems($sf,$view)

Sunday, August 23, 2009

Setting the Out of Office (OOF) with powershell and the EWS Managed API

Along with the RC of Exchange 2010 this week the RC of the EWS Managed API made its debut. There are a number of changes some of which break a few of the scripts i've posted here there is full list of the changes on http://social.technet.microsoft.com/Forums/en-US/exchangesvrdevelopment/thread/7053c628-9227-4cd5-ba9d-ed6fe8d484cb. But the good thing is now OOF and Freebusy work and the DNS lookup for autodisover is another major improvement.

Setting the OOF is pretty easy with the API lets look at a few examples as before you first need to create a Service object and authenticate if you haven't used the EWS Managed API in Powershell before see this post as a primer. To use this OOF code you first need to download and install the RC version of the ManagedAPI from here.

So lets take it as read we have our $service object now to get the Offsettings its just one call with the email address of the Account you want to pull the settings for.

$oofSetting = $service.GetUserOofSettings("user@domain.com")

A good point to remember when using any of the availability service Call like OOF and FreeBusy you need to always use delegate Access as EWS impersonation doesn't work with the availability service.

To show the OOF state just look at the State property

$oofSetting.State.ToString()

To show the oof message you have to look at the InternalReply or ExternalReply properties

$oofSetting.InternalReply.ToString()
$oofSetting.ExternalReply.ToString()

To set the State and or the Message property you should first get the current setting modify the property you want to modify and then update the OOF.

eg to set the OOF of the currently logged on user you could use the following code.



$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())

$oofSetting = $service.GetUserOofSettings($aceuser.mail.ToString())
$oofSetting.State.ToString()
$oofSetting.InternalReply = new-object Microsoft.Exchange.WebServices.Data.OofReply("Test 123")
$oofSetting.ExternalReply.ToString()
$oofSetting.State = [Microsoft.Exchange.WebServices.Data.OofState]::Enabled
$service.SetUserOofSettings($aceuser.mail.ToString(), $oofSetting);

Saturday, August 22, 2009

Phone List AD GAL update utility – An alternate to bulk imports

If you have been administrating mail systems for a while (and then some) then you have probably had to do a bulk update or two of one or more AD properties like phone numbers and address information. Depending on the time you have and your skill at building scripts you may have had some good and not so good experiences at this. The frustrating thing can be a script you build for one problem maybe be completely useless for the next and you may find yourself again spending time you don’t have building another script. Well because I’ve had to do this one too many times I came up with the following little script that allows dynamic matching of columns in a CSV file to import data into Active Directory. The other thing this script does is actually checks the current value within AD as not to update an already existing property and it’s a latched script so doesn’t allow you to update anything without clicking yes. The later could get frustrating but it’s a lot less frustrating then trying to fix a poorly tested bulk import.

How does it work

Powershell has a great little commandlet called import-csv that make importing data from a CSV prospective pretty easy. The script will pick the first line as column headers and then populate enough drop down Gui elements so you can map as many of the fields you want from the CSV file to AD properties.

Primary Mapping Field

This is the field that is used to identify the AD object to update note the field you select here doesn't get updated. In this field you generally need to pick a field that can be used to unique identify the accounts you want to update. The best to use generally is the Mail property mostly because phone lists generally always contain this information. What happens when the script runs is it uses this information to create a dynamic ADSI query based on the values that you select. Eg if you select the mail property it will try to find the AD account to update by doing a search for any user account with this email address, note if you want to do object other then AD useraccount you will need to fiddle with the LDAP query.

Update fields

I've limited the update fields to fields that are reasonably safe to to do a bulk update on instead of reusing the dynamic map from the primary map. If you want to add any fields you need to add the exact name of the Ldap attribute to the list eg

$ADprops.Add("homePhone",0)
$ADprops.Add("mobile",0)

(the 0 is just a blank for the hashtable.)

I've put a download of this code here

The code itself looks like

[System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")
[void][System.Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic')
$form = new-object System.Windows.Forms.form

$Filecolumns = @{ }
$ADprops = @{ }
$DropDownValues = @{ }
$mbcombCollection = @()

function UpdateAD(){
$dd = "Name"
Import-Csv $fileOpen.FileName.ToString() foreach-object{
#Find User
$root = [ADSI]'LDAP://RootDSE'
$dfDefaultRootPath = "LDAP://" + $root.DefaultNamingContext.tostring()
$dfRoot = [ADSI]$dfDefaultRootPath
$gfGALQueryFilter = "(&(&(&(& (mailnickname=*)(objectCategory=person)(objectClass=user)(" + $ppDrop.SelectedItem.ToString() + "=" + $_.($ppDrop1.SelectedItem.ToString()).ToString() + ")))))"
$dfsearcher = new-object System.DirectoryServices.DirectorySearcher($dfRoot)
$dfsearcher.Filter = $gfGALQueryFilter
$updateString = ""
$srSearchResult = $dfsearcher.FindOne()
if ($srSearchResult -ne $null){
$uoUserobject = $srSearchResult.GetDirectoryEntry()
Write-host $uoUserobject.DisplayName
foreach($mapping in $mbcombCollection){
if ($mapping.CSVField.SelectedItem -ne $null){
$updateString = $updateString + "User : " + $uoUserobject.DisplayName.ToString() + "`r`n"
$nval = $_.($mapping.CSVField.SelectedItem.ToString()).ToString()
$updateString = $updateString + "Property : " + $mapping.ADField.SelectedItem.ToString() + " Current Value : " + $uoUserobject.($mapping.ADField.SelectedItem.ToString()).ToString() + "`r`n"
$updateString = $updateString + "Update To Value : " + $_.($mapping.CSVField.SelectedItem.ToString()).ToString()
if (($uoUserobject.($mapping.ADField.SelectedItem.ToString()).ToString()) -ne $nval){
$result = [Microsoft.VisualBasic.Interaction]::MsgBox($updateString , 'YesNo,Question', "Confirm Change")
switch ($result) {
'Yes' {
$uoUserobject.($mapping.ADField.SelectedItem.ToString()) = $nval
$uoUserobject.setinfo()
}

}
}
}

}
}

}
}


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

# Add Primary Prop Drop Down
$ppDrop = new-object System.Windows.Forms.ComboBox
$ppDrop.Location = new-object System.Drawing.Size(190,40)
$ppDrop.Size = new-object System.Drawing.Size(200,30)
foreach ($prop in $aceuser.psbase.properties){
foreach($name in $prop.PropertyNames){
if ($name -ne $null){$ppDrop.Items.Add($name)}

}
}
$ADprops.Add("cn",0)
$ADprops.Add("sn",0)
$ADprops.Add("c",0)
$ADprops.Add("l",0)
$ADprops.Add("st",0)
$ADprops.Add("title",0)
$ADprops.Add("postalCode",0)
$ADprops.Add("postOfficeBox",0)
$ADprops.Add("physicalDeliveryOfficeName",0)
$ADprops.Add("telephoneNumber",0)
$ADprops.Add("facsimileTelephoneNumber",0)
$ADprops.Add("givenName",0)
$ADprops.Add("displayName",0)
$ADprops.Add("co",0)
$ADprops.Add("department",0)
$ADprops.Add("streetAddress",0)
$ADprops.Add("extensionAttribute1",0)
$ADprops.Add("mailNickname",0)
$ADprops.Add("wWWHomePage",0)
$ADprops.Add("name",0)
$ADprops.Add("countryCode",0)
$ADprops.Add("ipPhone",0)
$ADprops.Add("homePhone",0)
$ADprops.Add("mobile",0)

$form.Controls.Add($ppDrop)

# Add Primary Prop Drop Down
$ppDrop1 = new-object System.Windows.Forms.ComboBox
$ppDrop1.Location = new-object System.Drawing.Size(20,40)
$ppDrop1.Size = new-object System.Drawing.Size(150,30)

$fileOpen = New-Object System.Windows.Forms.OpenFileDialog
$fileOpen.InitialDirectory = $Directory
$fileOpen.Filter = "csv files (*.csv)*.csv"
$fileOpen.Title = "Import File"
$fileOpen.ShowDialog()
$fileOpen.FileName

Import-Csv $fileOpen.FileName.ToString() select -first 1 %{$_.PSObject.Properties} foreach-object {
$Filecolumns.add($_.name.ToString(),0)
}
$Filecolumns.GetEnumerator() sort name foreach-object {
$ppDrop1.Items.Add($_.Key.ToString())
}
$form.Controls.Add($ppDrop1)

$dloc = 120

$Filecolumns.GetEnumerator() foreach-object {
$mbcomb = "" select CSVField,ADfield
$dloc = $dloc + 25
$ppDrop2 = new-object System.Windows.Forms.ComboBox
$ppDrop2.Size = new-object System.Drawing.Size(200,30)
$ppDrop2.Location = new-object System.Drawing.Size(190,$dloc)
$ADprops.GetEnumerator() sort name foreach-object {
$ppDrop2.Items.Add($_.Key.ToString())
}
$mbcomb.ADfield = $ppDrop2

$form.Controls.Add($ppDrop2)
$ppDrop3 = new-object System.Windows.Forms.ComboBox
$ppDrop3.Size = new-object System.Drawing.Size(150,30)
$ppDrop3.Location = new-object System.Drawing.Size(20,$dloc)
$Filecolumns.GetEnumerator() sort name foreach-object {
$ppDrop3.Items.Add($_.Key.ToString())
}
$mbcomb.CSVField = $ppDrop3
$form.Controls.Add($ppDrop3)
$mbcombCollection += $mbcomb
}


$Gbox = new-object System.Windows.Forms.GroupBox
$Gbox.Location = new-object System.Drawing.Size(10,5)
$Gbox.Size = new-object System.Drawing.Size(400,100)
$Gbox.Text = "Primary Mapping field"
$form.Controls.Add($Gbox)

$Gbox = new-object System.Windows.Forms.GroupBox
$Gbox.Location = new-object System.Drawing.Size(10,120)
$Gbox.Size = new-object System.Drawing.Size(400,800)
$Gbox.Text = "Update Fields "
$form.Controls.Add($Gbox)

# Add Export MB Button

$exButton1 = new-object System.Windows.Forms.Button
$exButton1.Location = new-object System.Drawing.Size(420,10)
$exButton1.Size = new-object System.Drawing.Size(125,20)
$exButton1.Text = "Update AD"
$exButton1.Add_Click({UpdateAD})
$form.Controls.Add($exButton1)


$form.Text = "AD Phone List Update GUI"
$form.size = new-object System.Drawing.Size(1000,700)
$form.autoscroll = $true
$form.Add_Shown({$form.Activate()})
$form.ShowDialog()

Saturday, August 01, 2009

Twitter your Exchange Calendar reminders using Powershell and the EWS Managed API

As twitter is now more a part of the everyday rather than a flash in the pan it does give some new options for solving those time old problems the annoy us all. One of these is missing your meeting reminders or making sure your team or company don’t forget about any important calendar events if you have a company calendar or mailbox. Exchange and Outlook have always had a good functionality for tracking and setting reminders but human nature has something to do with us ignoring or not receiving them.

So how can scripting and a little EWS code solve this well it first has to start with a query of the calendar where the appointments you want to search against are located. Because reminders can be set for 2 weeks before an appointment you need to query for appointments during this time frame. Then filter anything thats set to private and you have some appointments that might have reminders you need to twitter. But before this you first need to use a little maths

x = minutesbeforeappointment or the calendar appointments .ReminderMinutesBeforeStart
y = Appointment StartTime
n = The Current time

so if (y-x) > n and (y-x)< n+15 and the last twitter anouncement doesn't have the same subject Twitter the subject of the appointment with the Start DateTime Attached. This means that if the appointment reminders is due in the next 15 minutes it will be twiitered. eg if the appoinment starts at 8:00 AM and has a 30 minute reminder then between 7:15 and 7:30 if this script was run it would twitter the reminder

Before running this script you need to set the email address of the mailbox where the calendar you want to twitter is located (if its a public folder then you would need to add some code to find that folderID first). And you also need to set the Twitter UserName and Password

eg

$MailboxName = "company@yourdomain.com"
$twitterusername = "username"
$twitterpassword = "password"


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

$MailboxName = "user@domain.com"

$twitterusername = "userName"
$twitterpassword = "password"


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()
}

$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())

$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
$cvCalendarview.StartDate = [System.DateTime]::Now
$cvCalendarview.EndDate = [System.DateTime]::Now.AddDays(14)
$cvCalendarview.MaxItemsReturned = 200;
$cvCalendarview.PropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)

$upset = 0
$statusup = 0

$frCalendarResult = $CalendarFolder.FindAppointments($cvCalendarview)

foreach ($apApointment in $frCalendarResult.Items){

if ($apApointment.Sensitivity -ne [Microsoft.Exchange.WebServices.Data.Sensitivity]::Private -band $apApointment.IsReminderSet -eq $true){
$ReminderSendTime = $apApointment.Start.AddMinutes(-$apApointment.ReminderMinutesBeforeStart)
if ($ReminderSendTime -ge [System.DateTime]::Now -band $ReminderSendTime -le [System.DateTime]::Now.AddMinutes(15))
{
$twitString = $apApointment.Subject.ToString()
if ($twitString.Length -gt 140){$twitString = $twitString.Substring(0,110)}
$twitString = $twitString + " Starts : " + $apApointment.Start.ToString("yyyy-MM-dd HH:mm:ss") + " "
[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"}