Thursday, June 11, 2009

Simple Exchange Email client for Powershell using the EWS Managed API

One thing that is useful now and again when you are testing different problems and configurations on Exchange is to have a Mail client that isn’t Outlook or OWA. Back in the days there used to be a Simple Mapi client called the Exchange client which disappeared after 5.0. While this isn’t a attempt to replace it or to be an end user mail client at all it’s a great little test bench script that allows you to get in and look at mailbox and the items in that mailbox, Download attachments,export single emails, look at Message headers, search for emails and if there is any particular problem you want to tackle in regards to certain properties it’s something that can be very easily adapted to fit an specific problem.

How it works

This script presents a Winform GUI that allows you to interact with a mailbox and present it into a displayable view back to a user. Okay I could go on like this all day (seriously!!) this is actually what it looks like when you fire it up.



First you need to put the Email address of the mailbox you want to open then click open mailbox it will then attempt to do an autodiscover. Why Autodiscover is great it doesn’t always work so there is the ability to put the CAS URL for ews eg
“https://servername.com/ews/exchange.asmx”.

Use default credentials means the script will try to use the currently logged on user when accessing the mailbox otherwise you can fill in the credential settings.

When you hit the open mailbox button the script does a Deep folder Traversal starting at the Mailbox root and then builds a foldertree of the result. An event is added to the folder tree so when you click any of the leaves it will fire an event that then does a Findfolder operation to retrieve items from the selected folder. It will only return the number of items listed in the number box which is 100 by default so if you where to click the inbox it will only return the first 100 items if you want it to return more or less you need to change this number value. The search functions allow you to refine which items are returned or you can create different searches based on From, Subject and Body it does a Substring search when you select each of these properties. The search works by building the correct searchfilter string based on the dropdown value you select and the text that’s entered in the text box.

The ShowMessage button does a Get-Item operation and retrieves the message and body as Text and displays the result. The Show header lets you look at the Message header and the download Attachment and export message allows you to download message and attachments which I've described in another post here.

There is really on two requirements for this script the first is you need the have the EWS Managed API installed which you can download from here.The only other requirement is to have Powershell.

I've put a download of this script here

Sunday, June 07, 2009

Using SearchFilter and other Nested Types in the EWS Managed API from Powershell

A SearchFilter in the EWS Managed API gives you the ability to place restrictions on any findItem operations you do on a folder with Exchange Web Services. For instance you can search for a message in a folder based on a subject or words within the subject etc. Both the SearchFilter and Recurrence classes with the EWS Managed API are implemented as Nested Types see http://msdn.microsoft.com/en-us/library/ms229027.aspx for a description as to what these are. To use one of these objects within a NestedType you need to use the following format when initializing it namespace.enclosing type+nested type see http://weblogs.asp.net/soever/archive/2006/12/11/powershell-and-using-net-enum-types.aspx

So to declare a IsEqualTo filter use

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


For a ContainsSubstring filter use

$view.SearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+ContainsSubstring([Microsoft.Exchange.WebServices.Data.ItemSchema]::Subject,”string to search”)

To put that in a example to search for any messages from a certain email address you could use

$searchemail = “mailtosearch@domaim.com"
$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())
$inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox)
"Number or Unread Messages : " + $inbox.UnreadCount
$view = New-Object Microsoft.Exchange.WebServices.Data.ItemView(1)
$view.SearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+ContainsSubstring ([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::Sender, searchemail)
$findResults = $service.FindItems([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$view)
""
"Last Mail From : " + $findResults.Items[0].From.Name
"Subject : " + $findResults.Items[0].Subject
"Sent : " + $findResults.Items[0].DateTimeSent

Downloading Attachments and Exporting Email as eml files in the EWS Managed API using Powershell

A common and useful thing you may want to do in Powershell when working with Exchange Email automation is to download an attachment. When working with Exchange Web Services you first need to use a GetItem operation to work out what attachments are on a message then use a GetAttachment operation which returns a Byte Array of each attachments content. So all you need to do with this Byte array is write it out to a file using the System.IO.FileStream Class.

To do this you first need a message object that you would normally get doing a GetItem operation in EWS in the managed API its just one line once you know the MessageID. Eg

$msMessage = [Microsoft.Exchange.WebServices.Data.EmailMessage]::Bind($service,$MessageID)


Once you have the message object you will have a list of attachments and the attachmentID you need to make a GetAttachment operations. Again this is pretty easy to do using the Managed API which extrapolates the GetAttachment operation as the load method on an attachment object.

$fileName = “C:\temp”
foreach($attach in $msMessage.Attachments){
$attach.Load()
$fiFile = new-object System.IO.FileStream(($fppath + “\” + $attach.Name.ToString()), [System.IO.FileMode]::Create)
$fiFile.Write($attach.Content, 0, $attach.Content.Length)
$fiFile.Close()
write-host "Downloaded Attachment : " + (($fppath + “\” + $attach.Name.ToString())
}


In Exchange Web Services you have the ability to export the whole message in a serialized EML format (this is different from a Compound message format msg file which contains all the Mapi properties related to the rich types of the Exchange store). To do this when you do the original Get-Item operation request for the message you need to make sure you also request the MIME content of the message. This MIME content will contain the RFC serialized copy of the message which you can write out to a file. Eg

$psPropset = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.ItemSchema]::MimeContent)
$msMessage = [Microsoft.Exchange.WebServices.Data.EmailMessage]::Bind($service,$MessageID,$psPropset)
Then similar to the above attachment example you can write that MIME content out to filestream. Eg
$fileName = “C:\temp\exportedmail.eml”
$fiFile = new-object System.IO.FileStream($fileName, [System.IO.FileMode]::Create) $fiFile.Write($msMessage.MimeContent.Content, 0,$msMessage.MimeContent.Content.Length)
$fiFile.Close()

Sunday, May 24, 2009

Mail Enabling a Public folder with the EWS Managed API and finding and changing the primary email address without using Exchange cmdlets

If your working with Public Folders in the EWS Managed API then you may want to mailenable a new or existing Public Folder without using EMS cmdlets. I described a method that you can use to mail enable Public folders using EWS a while ago here and this same method will work with a little bit of different code. Like the previous post you need to set the following 2 properties to mail enabled a folder.

PR_PUBLISH_IN_ADDRESS_BOOK = 0x3FE6000B
PR_PF_PROXY_REQUIRED = 0x671F000B

So if you want to mailenable an existing folder that you have the path for say its "firstlevel/secondlevel/newfolder" you will first need to have some code that will find the folder based on the path by doing a search. Then once you have the folder ID you can Bind to the folder and set those Mapi properties. Using a UpdateFolder operation was one of the harder operations in normal EWS proxy code but its now so much easier to do in the EWS Managed API.

Once you have set these properties a backgroup process will create the AD Proxy object for this public folder which will contain all the email address's assigned to this and any other folder specific setting such a limits etc. If you want to connect to this proxy object and change things like limits and email address you can use LDAP directly without using the EMS cmdlets. A little word or warning if you are going to be setting email addresses directly you should make sure you implement code that ensures you dont create duplicate email addresses. To find the ADProxy object you can get the AD GUID of the proxy object by retrieving the following property via EWS and then use this GUID value to access the AD object.

PR_PF_PROXY = 0x671D

This is a binary property which will be returned base64 encoded so you first need to decode this and the convert it to hex to use in a System.DirectoryServices.

So the code to MailEnable a folder using the EWS Managed API looks like

String fpfolderPath = "";
ExtendedPropertyDefinition PR_PF_PROXY = new ExtendedPropertyDefinition(0x671D, MapiPropertyType.Binary);
PropertySet PsPropertySet = new PropertySet(PR_PF_PROXY);
PsPropertySet.BasePropertySet = BasePropertySet.FirstClassProperties;
Folder PfRoot = Folder.Bind(service, WellKnownFolderName.PublicFoldersRoot,PsPropertySet);
String[] faFldArray = fpfolderPath.Split('/');
Folder tfTargetFolder = PfRoot;
for (int lint = 1; lint < fview =" new" searchfilter =" new" propertyset =" PsPropertySet;" ffresult =" service.FindFolders(tfTargetFolder.Id," totalcount ="=" tftargetfolder =" ffResult.Folders[0];" pr_publish_in_address_book =" new" pr_pf_proxy_required =" new">


To access the AD proxy object using the FolderID from the prevous piece of code


ExtendedPropertyDefinition PR_PF_PROXY = new ExtendedPropertyDefinition(0x671D, MapiPropertyType.Binary);
PropertySet PsPropertySet = new PropertySet(PR_PF_PROXY);
Folder meMailEnabledFolder = Folder.Bind(service, tfTargetFolder.Id, PsPropertySet);
String pfProxy = "";
tfTargetFolder.Update();
byte[] pfPro = Convert.FromBase64String(meMailEnabledFolder.ExtendedProperties[0].Value.ToString());
pfProxy = BitConverter.ToString(pfPro);
DirectoryEntry deDirEnt = new DirectoryEntry(("LDAP://<GUID=" + pfProxy.Replace("-", "") + ">"));
PropertyValueCollection proxyAddresses = deDirEnt.Properties["proxyAddresses"];
if (proxyAddresses != null)
{
for (int ipar = 0; ipar < proxyAddresses.Count; ipar++)
{
Console.WriteLine(proxyAddresses[ipar].ToString());
if (proxyAddresses[ipar].ToString().Contains("SMTP:"))
{
proxyAddresses[ipar] = "SMTP:address@domain.com.au";
}
}
}
deDirEnt.CommitChanges();

I've put a download of this code here

Friday, May 22, 2009

Acknowledging Appointment reminders with WebDAV

One thing that can be little tricky if your working in WebDAV is dealing with appointment reminders, especially when you have to deal with recurring appointments. If you dont use the right method its easy to break the recurrance of appointments. The best way of working something like this out is have a look at the way OWA does it and then just use the same xml. So basically what you end up with is firstly a method to acknowledge non-recurring appointment reminders by removing the reminderoffset property. And a method to acknowledge recurring appointment reminders by setting the remindernexttime property. I've put a download of this VBS script here the code looks like
eg

set req = Createobject("microsoft.xmlhttp")

CalendarURL = "https://servername/exchange/mailbox/calendar"
AppointmentURL = CalendarURL & "/appointment.eml"

If RecurringAppointment = false Then
xmlstr = "<?xml version=""1.0""?><a:propertyupdate xmlns:a=""DAV:""><a:remove><a:prop>" _
& "<d:reminderoffset xmlns:d=""urn:schemas:calendar:"" /></a:prop></a:remove>" _
& "<a:target><a:href>" & AppointmentURL & "</a:href></a:target></a:propertyupdate>"
else
xmlstr = "<?xml version=""1.0""?><a:propertyupdate xmlns:a=""DAV:""><a:set><a:prop>" _
& "<d:remindernexttime xmlns:d=""urn:schemas:calendar:"">4501-01-01T00:00:00.000Z</d:remindernexttime>" _
& "</a:prop></a:set><a:target><a:href>" & AppointmentURL & "</a:href></a:target></a:propertyupdate>"
End if

req.open "BPROPPATCH", CalendarURL, False
req.setRequestHeader "Content-Type", "text/xml;"
req.setRequestHeader "Translate", "f"
req.setRequestHeader "Content-Length:", Len(xmlstr)
req.send(xmlstr)
If (req.Status >= 200 And req.Status < 300) Then
Wscript.echo "Reminder Acknowledged"
else
Wscript.echo "Request Failed. Results = " & req.Status & ": " & req.statusText
End If

Saturday, May 09, 2009

Mailbox Size Summary reporting Gui for Exchange 2007

Picking up again on the topic i touched on in January about creating different summary reports of how size and resources are consummed within an Exchange enviroment he's a script that takes it in another direction (and dont we need one of those). A number of people have asked questions about how they can report on mailbox usage not just based on particular users but usage by particular OU or other User property eg Department,Office etc. An here's the script to do it

How it works


This script first combines 3 different Exchange cmdlets to build a displayable and redisplayble dataset that is stored in hashtable. The Get-User cmdlet is used to get all the user information and a number of properties such as the ExchangeGUID,MailboxSize and what OU a users is in is addded to the standard Get-User object and then the object is stored in a hashtable indexed by the Active Directory GUI. Because you can't link directly Get-User and Get-Mailboxstatistics directly without using Pipeline composition which will do a search for each user making this script a little to slow for enviroments with a large number of users. Get-Mailbox is used to create a hashtable that allows the linking of the information that is returned by the Get-User cmdlet and what is returned by the Get-Mailboxstatistics.

To build display elements all the properties from Get-User are added to the Dropdown for Groupby type. When this is selected it fires a function that causes the data stored in the results hashtable to be regrouped using the selected value which then rebuilds the data in the datatable which in tern refreshs what you see in the datagrid. To spice things up a bit I've added some extra functionality like being able to get the actally mailboxes that are being included in the grouping in a seperate datatable. Also the ability to export both grids to a csv file. The final things to give this script some visual spark by adding a few Google charts to add a visual dimension to the summariesed data

The result is a script that produces a lot of quite interesting and at times slightly amsuing results. Eg being able to see grouped by first name who uses the most mailbox resources is kind of funny. You could go take this little further use the persons birthday to figure their star sign and then show mailbox size usage by signs of the zodiac.

(If your looking for OU its added as the last item in the Drop down list)

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")
$form = new-object System.Windows.Forms.form

$usrinfo = @{ }
$ctab = @{ }
$agDepartment = @{ }
$agOffice = @{ }
$htAg = @{ }
$ExcomCollection = @()
function Aggresults{
$htAg.clear()
if ($mtTypeDrop.SelectedItem -eq $null){
$agMethod = "LastName"
}
else{
$agMethod = $mtTypeDrop.SelectedItem.ToString()
}
foreach ($row in $usrinfo.Values){
if ($row.$agMethod.ToString() -eq ""){
if ($htAg.containskey("Not Set")){
$htAg["Not Set"].Number = $htAg["Not Set"].Number + 1
$htAg["Not Set"].Size = $htAg["Not Set"].Size + [INT]$row.MailboxSize.ToString()
}
else{
$mbcnt = "" | select Name,Number,Size
$mbcnt.Name = "Not Set"
$mbcnt.Number = 1
$mbcnt.Size = [INT]$row.MailboxSize.ToString()
$htAg.Add("Not Set",$mbcnt)

}
}
else{
if ($htAg.containskey($row.$agMethod.ToString())){
$htAg[$row.$agMethod.ToString()].Number = $htAg[$row.$agMethod.ToString()].Number + 1
$htAg[$row.$agMethod.ToString()].Size = $htAg[$row.$agMethod.ToString()].Size + [INT]$row.MailboxSize.ToString()
}
else{
$row.$agMethod.ToString()
$mbcnt = "" | select Name,Number,Size
$mbcnt.Name = $row.$agMethod.ToString()
$mbcnt.Number = 1
$mbcnt.Size = [INT]$row.MailboxSize.ToString()
$htAg.Add($row.$agMethod.ToString(),$mbcnt)
}
}
}
$agtable.Clear()
$valueBlock = ""
$TitleBlock = ""
$TitleBlock1 = ""
foreach ($row in $htAg.Values){
$agtable.rows.add($row.Name,$row.Number,$row.Size)
if ($valueBlock -eq ""){$valueBlock = $row.Size.ToString()
$lval = [INT]$row.Size
}
else {
$valueBlock = $valueBlock + "," + $row.Size.ToString()
if ($lval -lt [INT]$row.Size){$lval = [INT]$row.Size}
}
if ($TitleBlock -eq ""){$TitleBlock = $row.Name.ToString().Replace("&","-")
$TitleBlock1 = $row.Name.ToString().Replace("&","-")
}
else {$TitleBlock = $TitleBlock + "|" + $row.Name.ToString().Replace("&","-")
$TitleBlock1 = $row.Name.ToString().Replace("&","-") + "|" + $TitleBlock1
}
}
$csString = "http://chart.apis.google.com/chart?cht=p3&chs=430x160&chds=0," + $lval + "&chd=t:" + $valueBlock + "&chl=" + $TitleBlock + "&chco=0000ff,00ff00,ff0000,FFFFFF,000000"
$pbox.ImageLocation = $csString
$csString1 = "http://chart.apis.google.com/chart?cht=bhs&chs=530x300&chd=t:" + $valueBlock + "&chds=0," + ($lval+20) + "&chxt=x,y&chbh=a&chxr=" + "&chxr=0,0," + ($lval+20) + "&chxl=1:|" + $TitleBlock1 + "&chco=0000ff,00ff00,ff0000,FFFFFF,000000"
$pbox1.ImageLocation = $csString1
$dgDataGrid.DataSource = $agtable
}

function getMailboxSizes(){

$mbcombCollection = @()
$usrinfo.clear()
$ctab.clear()
$agDepartment.clear()
$agOffice.clear()
$ExcomCollection = @()
$agtable.clear()

$serverName = $snServerNameDrop.SelectedItem.ToString()
if ($snServerNameDrop.SelectedItem.ToString() -eq "ALL Servers"){
$mailboxes = get-mailbox -ResultSize Unlimited
}
else{
$mailboxes = get-mailbox -server $snServerNameDrop.SelectedItem.ToString() -ResultSize Unlimited
}
$mailboxes | foreach-object{
$ctab.add($_.Guid.ToString(),$_.ExchangeGuid)
}
"Finished Get Mailbox"
Get-User -recipienttype UserMailbox -ResultSize Unlimited | foreach-object{
if ($ctab.ContainsKey($_.Guid.ToString())){
$usrobj = $_ | select *,MailboxSize,ExchangeGUID,OU
$oustring = ""
$idarry = $_.Identity.ToString().Split("/")
for($i=1;$i-lt ($idarry.length-1);$i++){
$oustring = $oustring + "\" + $idarry[$i]
}
$usrobj.OU = $oustring
$usrobj.ExchangeGUID = $ctab[$_.Guid.ToString()]
$usrinfo.add($ctab[$_.Guid.ToString()].ToString(),$usrobj)
}

}
$mbServers = get-mailboxserver
if ($snServerNameDrop.SelectedItem.ToString() -eq "ALL Servers"){
$mbServers | foreach-object{

$mscombCollection += get-mailboxstatistics -server $_.Name | Where {$_.DisconnectDate -eq $null}

}
}
else{
$mscombCollection += get-mailboxstatistics -server $snServerNameDrop.SelectedItem.ToString() | Where {$_.DisconnectDate -eq $null}
}

$mscombCollection | ForEach-Object{
$usrobj = $usrinfo[$_.MailboxGuid.ToString()]
if ($usrinfo.ContainsKey($_.MailboxGuid.ToString())){
if ($_.TotalItemSize.Value.ToMB() -ne $null){
$usrobj.MailboxSize = $_.TotalItemSize.Value.ToMB()
}
else
{
$usrobj.MailboxSize = 0
}
}

}
Aggresults
}

function ShowMailboxes(){
if ($mtTypeDrop.SelectedItem -eq $null){
$agMethod = "LastName"
}
else{
$agMethod = $mtTypeDrop.SelectedItem.ToString()
}
$mbtable = New-Object System.Data.DataTable
$mbtable.TableName = "Mailboxes"
$mbtable.Columns.Add("Display Name")
$mbtable.Columns.Add($agMethod)
$mbtable.Columns.Add("Mailbox Size(MB)",[int64])
foreach ($row in $usrinfo.Values){
if($row.$agMethod -eq $agtable.DefaultView[$dgDataGrid.CurrentCell.RowIndex][0]){
$mbtable.rows.add($row.DisplayName,$row.$agMethod,$row.MailboxSize)
}
else{
if($row.$agMethod -eq ""){
if("Not Set" -eq $agtable.DefaultView[$dgDataGrid.CurrentCell.RowIndex][0]){
$mbtable.rows.add($row.DisplayName,$row.$agMethod,$row.MailboxSize)
}
}
}
}
$dgDataGrid1.DataSource = $mbtable
}

function ExportGrid1csv{

$exFileName = new-object System.Windows.Forms.saveFileDialog
$exFileName.DefaultExt = "csv"
$exFileName.Filter = "csv files (*.csv)|*.csv"
$exFileName.InitialDirectory = "c:\temp"
$exFileName.ShowHelp = $true
$exFileName.ShowDialog()
if ($exFileName.FileName -ne ""){
$logfile = new-object IO.StreamWriter($exFileName.FileName,$true)
$logfile.WriteLine("DisplayName,Number of Mailboxes,Mailbox Size(MB)")
foreach($row in $agTable.Rows){
$logfile.WriteLine("`"" + $row[0].ToString() + "`"," + $row[1].ToString() + "," + $row[2].ToString())
}
$logfile.Close()
}
}

function ExportGrid2csv{

$exFileName = new-object System.Windows.Forms.saveFileDialog
$exFileName.DefaultExt = "csv"
$exFileName.Filter = "csv files (*.csv)|*.csv"
$exFileName.InitialDirectory = "c:\temp"
$exFileName.ShowHelp = $true
$exFileName.ShowDialog()
if ($exFileName.FileName -ne ""){
$logfile = new-object IO.StreamWriter($exFileName.FileName,$true)
$logfile.WriteLine("DisplayName,Office,Department,Mailbox Size(MB)")
foreach($row in $mbtable.Rows){
$logfile.WriteLine("`"" + $row[0].ToString() + "`"," + $row[1].ToString() + "," + $row[2].ToString())
}
$logfile.Close()
}
}

$agtable = New-Object System.Data.DataTable
$agtable.TableName = "Mailbox Sizes"
$agtable.Columns.Add("Name")
$agtable.Columns.Add("# Mailboxes",[int64])
$agtable.Columns.Add("Mailbox Size(MB)",[int64])




# Add Server DropLable
$snServerNamelableBox = new-object System.Windows.Forms.Label
$snServerNamelableBox.Location = new-object System.Drawing.Size(10,20)
$snServerNamelableBox.size = new-object System.Drawing.Size(80,20)
$snServerNamelableBox.Text = "ServerName"
$form.Controls.Add($snServerNamelableBox)

# Add Server Drop Down
$snServerNameDrop = new-object System.Windows.Forms.ComboBox
$snServerNameDrop.Location = new-object System.Drawing.Size(90,20)
$snServerNameDrop.Size = new-object System.Drawing.Size(100,30)
get-mailboxserver | ForEach-Object{$snServerNameDrop.Items.Add($_.Name)}
$snServerNameDrop.Items.Add("ALL Servers")
$snServerNameDrop.Add_SelectedValueChanged({getMailboxSizes})

$form.Controls.Add($snServerNameDrop)

# Add Agregate Type DropLable
$mtTypeDroplableBox = new-object System.Windows.Forms.Label
$mtTypeDroplableBox.Location = new-object System.Drawing.Size(200,20)
$mtTypeDroplableBox.size = new-object System.Drawing.Size(70,20)
$mtTypeDroplableBox.Text = "Group By"
$form.Controls.Add($mtTypeDroplableBox)

# Add Mailbox Type Drop Down
$mtTypeDrop = new-object System.Windows.Forms.ComboBox
$mtTypeDrop.Location = new-object System.Drawing.Size(270,20)
$mtTypeDrop.Size = new-object System.Drawing.Size(135,30)
$properties = get-user -resultsize 1 | get-member -membertype Property
foreach ($prop in $properties){
$mtTypeDrop.Items.Add($prop.Name)
}
$mtTypeDrop.Items.Add("OU")
$mtTypeDrop.Add_SelectedValueChanged({Aggresults})
$form.Controls.Add($mtTypeDrop)

# Show Mailboxs Button

$shMailboxes = new-object System.Windows.Forms.Button
$shMailboxes.Location = new-object System.Drawing.Size(600,19)
$shMailboxes.Size = new-object System.Drawing.Size(120,23)
$shMailboxes.Text = "Show Mailboxes"
$shMailboxes.visible = $True
$shMailboxes.Add_Click({ShowMailboxes})
$form.Controls.Add($shMailboxes)

# Add Export Grid Button

$exButton1 = new-object System.Windows.Forms.Button
$exButton1.Location = new-object System.Drawing.Size(430,19)
$exButton1.Size = new-object System.Drawing.Size(90,23)
$exButton1.Text = "Export Grid"
$exButton1.Add_Click({ExportGrid1csv})
$form.Controls.Add($exButton1)

# Add Export Grid Button 2

$exButton2 = new-object System.Windows.Forms.Button
$exButton2.Location = new-object System.Drawing.Size(750,19)
$exButton2.Size = new-object System.Drawing.Size(125,23)
$exButton2.Text = "Export Grid"
$exButton2.Add_Click({ExportGrid2csv})
$form.Controls.Add($exButton2)

#add Picture box

$pbox = new-object System.Windows.Forms.PictureBox
$pbox.Location = new-object System.Drawing.Size(550,360)
$pbox.Size = new-object System.Drawing.Size(500,220)
$form.Controls.Add($pbox)

$pbox1 = new-object System.Windows.Forms.PictureBox
$pbox1.Location = new-object System.Drawing.Size(10,360)
$pbox1.Size = new-object System.Drawing.Size(550,370)
$form.Controls.Add($pbox1)

# Add DataGrid View

$dgDataGrid = new-object System.windows.forms.DataGridView
$dgDataGrid.Location = new-object System.Drawing.Size(10,50)
$dgDataGrid.size = new-object System.Drawing.Size(530,300)
$dgDataGrid.AutoSizeRowsMode = "AllHeaders"
$form.Controls.Add($dgDataGrid)

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

$form.Text = "Exchange 2007 Group by Mailbox Size Form"
$form.size = new-object System.Drawing.Size(1000,700)
$form.autoscroll = $true
$form.Add_Shown({$form.Activate()})
$form.ShowDialog()

Thursday, April 30, 2009

Add Delegates to a Mailbox with Powershell and the EWS Managed API

Someone emailed me about this one today and its one thing that can now easily be solved using the EWS Managed API. There’s already a good C# sample on MSDN for doing this http://msdn.microsoft.com/en-us/library/dd633621.aspx. To convert this to something you can use in Powershell once you have downloaded and installed the EWS Managed API would look like the following below. This example sets the calendarpermissions to editor and the inbox to read for full details of what other permissions you can set have a look at the SDK.

$delegatetoAdd = "delage@youdomain.com"

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

$mbMailbox = new-object Microsoft.Exchange.WebServices.Data.Mailbox($aceuser.mail.ToString())
$dgUser = new-object Microsoft.Exchange.WebServices.Data.DelegateUser($delegatetoAdd)
$dgUser.ViewPrivateItems = $false
$dgUser.ReceiveCopiesOfMeetingMessages = $false
$dgUser.Permissions.CalendarFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::Editor
$dgUser.Permissions.InboxFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::Reviewer
$dgArray = new-object Microsoft.Exchange.WebServices.Data.DelegateUser[] 1
$dgArray[0] = $dgUser
$service.AddDelegates($mbMailbox, [Microsoft.Exchange.WebServices.Data.MeetingRequestsDeliveryScope]::DelegatesAndMe, $dgArray);



This script will add a delegate to the currently loged on user but this is something you may want to use impersonation for see http://msdn.microsoft.com/en-us/library/bb204095.aspx if you wanted to add a delegate to possiblly a large number of users or if you where doing this during mailbox provisioning. So this version of the script will use impersonation to access another users mailbox and add a delegate.

$mbtoDelegate = "user@domain.com"
$delegatetoAdd = "delegate@domain.com"


$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())
$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $mbtoDelegate);

$mbMailbox = new-object Microsoft.Exchange.WebServices.Data.Mailbox($mbtoDelegate)
$dgUser = new-object Microsoft.Exchange.WebServices.Data.DelegateUser($delegatetoAdd)
$dgUser.ViewPrivateItems = $false
$dgUser.ReceiveCopiesOfMeetingMessages = $false
$dgUser.Permissions.CalendarFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::Editor
$dgUser.Permissions.InboxFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::Reviewer
$dgArray = new-object Microsoft.Exchange.WebServices.Data.DelegateUser[] 1
$dgArray[0] = $dgUser
$service.AddDelegates($mbMailbox, [Microsoft.Exchange.WebServices.Data.MeetingRequestsDeliveryScope]::DelegatesAndMe, $dgArray);

I've put a download of the two scripts here

Friday, April 17, 2009

Using the EWS Managed API with powershell

If you missed it the first public beta of Exchange 2010 was released this week while I'm not one for getting two excited over beta's there was one other important release this week which was the beta release of the EWS Managed API. Why this is a little more exciting then a new version of Exchange is that you can actually start using this now to access Exchange Web Services on a Exchange 2007 Server. What makes this component a little more significant if you have done any programming in EWS even if it just sending raw XML to and from the server you may have noticed that at times this can be difficult and the interface isn't that intuitive. So what the EWS Managed API sets out to do is provide you with a interface that makes writing Exchange code a more intuitive experience by essential normalising the logic and the interfaces you use. While there are still some inescapable realities of writing Exchange code like Mapi properties etc (If you have missed them there has been some great posts recently on Mapi properties on the Exchange blog see) this is a much needed addition.

For anybody who wants to write simple Powershell scripts that access Exchange mailbox data your going to love this API. Lets look at how to get started and then you can start buring some daylight.

Requirments

The EWS Managed API requires the workstation where your running this from to have .NET 3.5 installed. The RTM version of Powershell will work fine with 3.5 so there's no need for V2. You then need to download and Install the EWS Managed API first from here

Coding

First you need to load the DLL

$dllpath = "C:\Program Files\Microsoft\Exchange\Web Services\1.0\Microsoft.Exchange.WebServices.dll"
[void][Reflection.Assembly]::LoadFile($dllpath)


Now you can create a ExchangeService object if your going to be coding against 2007 you must explictly set the version.

$service = new-object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2007_SP1)

Depending on where your running this code if you can make use of Autodiscover then all you need to do is provide the email address and the component will autodicover the CAS server to use. eg

$service.AutodiscoverUrl("email@domain.com")

You may not however want to hardcode the users email address in a script so what you might want to do is grab the email address using ADSI by using the Users SID eg.

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

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

If you can't use Autodiscover and you want to hardcode the URI then you could use

$uri=[system.URI] "https://casservername/ews/exchange.asmx"
$service.Url = $uri

If you dont want to use the currently logged on users credentials then you can specifiy your own
$service.Credentials = New-Object System.Net.NetworkCredential("username","password","domain")

You can also make use of Impersonation if you so wish but i haven't got a sample for that yet. But once you have connected and authenticated against EWS you can then use some simple code to do a varitety of things here one sample of showing the number of unread email and that last received email's details

$inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox)
"Number or Unread Messages : " + $inbox.UnreadCount
$view = New-Object Microsoft.Exchange.WebServices.Data.ItemView(1)
$findResults = $service.FindItems([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$view)
""
"Last Mail From : " + $findResults.Items[0].From.Name
"Subject : " + $findResults.Items[0].Subject
"Sent : " + $findResults.Items[0].DateTimeSent

If your running this against a development server that has self signed certificates then there is another thing you need to add to your code to make it capable of dealing with self signed certificates. Because you cant use delegates in the RTM version of Powershell you need to use a workaround there is this one
or http://poshcode.org/624

For more samples have a look at Exchange development portal.


I've put a download of these scripts here

Friday, March 27, 2009

Sanitizing Get-LogonStatistics cmdlet data to view better Logon stats in Exchange 2007

If you have used the get-Logonstatistics cmdlet you will know that it’s a great tool for looking at who’s logged onto your Exchange server using Outlook but the results set that it returns can be a little awkward to use in a continuative way. This is way of saying there is a lot of noise in what is returned also because logons are happening at different times of the day if you are looking to log this information over a period of time so you can do statistical analysis of the data it can be a little difficult. So what this script does is first looks at sanitizing the data by only looking at the results from get-Logonstatistics that actually represent mailboxes and also reduces down each logon so it only shows one entry per IP/Client connection. To create files that can be used to look at logon statistics it records the results of each query in a daily file and uses some logic to find new logons and filter existing ones.

How it works

Like many of the scripts I post it uses a lot of different powershell functions to achieve different levels of functionality. It uses Import-csv and Export-csv to keep track of the history status as well as two hashtables are used with an aggregation key that filters down the result set of a Get-logonstatistic query down to a useable format. A get-mailbox query is also used to make sure that only logons that relate to actual mailboxes are included in the results.

Running the Script

When you run the script it takes one argument which is the name of the server you want to run it against. Eg.

.\ sanlog.ps1 yourservername

This is just a simple start but it gives a framework you can could use to create actual logon reports that record when users loged on and logged off and how long they where using resources on your server.

I’ve put a download of this script here the script itself looks like.

$ServerName = $args[0]

$ExcomCollection = @()
$MBHash = @{ }
$MBHistHash = @{ }

$HistoryDir = "c:\LogonHistory"

if (!(Test-Path -path $HistoryDir))
{
New-Item $HistoryDir -type directory
$frun = 1
}

$datetime = get-date
$fname = $script:HistoryDir + "\"
$fname = $fname + $datetime.ToString("yyyyMMdd") + $ServerName + "-LogonHist.csv"

Import-Csv ($fname) | %{
$idvalue = $_.identity.ToString()
$logonEvent = $_
$ltime = [datetime]::Parse($_.LogonTime)
if ($_.ClientIPAddress -eq $null){$agvalue = $_.identity.ToString() + $_.UserName + $ltime.ToString("hhmm").Substring(0,3)}
else{$agvalue = $_.identity.ToString() + $_.UserName + $_.ClientIPAddress}
if ($MBHistHash.Containskey($agvalue) -eq $false){
$MBHistHash.Add($agvalue,$_)
}

}


get-mailbox -server $ServerName -ResultSize Unlimited | foreach-object{
if ($MBHash.Containskey($_.LegacyExchangeDN.ToString()) -eq $false){
$MBHash.add($_.LegacyExchangeDN.ToString(),$_)
}
}
$LogonUnQ = @{ }

get-logonstatistics | foreach-object{
$idvalue = $_.identity.ToString()
$logonEvent = $_
$ltime = $_.LogonTime
if ($_.ClientIPAddress -eq $null){$agvalue = $_.identity.ToString() + $_.UserName + $ltime.ToString("hhmm").Substring(0,3)}
else{$agvalue = $_.identity.ToString() + $_.UserName + $_.ClientIPAddress}
if ($idvalue -ne $null){
if ($LogonUnQ.Containskey($agvalue) -eq $false){
if ($MBHash.Containskey($idvalue)){
$LogonUnQ.Add($agvalue,$logonEvent)
if ($MBHistHash.Containskey($agvalue) -eq $false){
$MBHistHash.Add($agvalue,$_)
}
else{
$ts = New-timeSpan $MBHistHash[$agvalue].LogonTime $ltime
if ($ts.minutes -gt 5){
$MBHistHash.Add($agvalue+$ltime,$_)

}
}
}

}
}
}
foreach ($row in $MBHistHash.Values){
$ExcomCollection += $row
}

$ExcomCollection | export-csv –encoding "unicode" -noTypeInformation $fname

Monday, March 02, 2009

Creating Emails based on a Twitter feed / Friends timeline status updates

Continuing on from my previous post about posting your current Exchange calendar appointments as twitter status updates this script explores some more creative uses of the Twitter and Exchange 2007 API's. This script will read your current Twitter account details and grab your friend’s timeline and then create emails in your inbox (or other folder of choice) for each update in your friends twitter timelines (or the people your following). To match up the Received time of the email with the time the status update is posted on Twitter the script sets the necessary mapi properties to make the message appear that it was received at the same time as the status update was posted. To keep track of which updates have been created as email it downloads the latest feed and then on each run it uses Xpath to read the XML file and loads a hashtable which is used to ensure that no status update is created as an email twice. To access Exchange and create the messages in the inbox from the twitter status update it uses some precanned Exchange Web Services code I’ve added to my EWSUtil powershell library.

How does it work?

A quick run through of the script logic is it will first check to see if there is a temp directory on the C:\ and then it will see if there is a file called twitstatus.xml in this directory. If there is no file then the script knows this is the first run and it won’t bother the check to see if the status updates being read from the friend’s timeline have already been created. If the twitstatus.xml file does exist the file is read and a hashtable of the status Id’s is created. The script then reads the twitter friends timeline feed for the configured account and then using Xpath runs through all the status updates in the feed it does a compare to make sure that your own feed Status updates are excluded and also checks the hashtable to ensure that duplicates aren’t created. If it determines that an email should be added from the status update it calls a EWSUtil method that will create a sent email in the folder specified it does a conversion of the internal Twitter date/time to a date/time that can be use used in .NET and Exchange and handles the conversion to UTC. The Twitter Status text is added to both the Subject and the Body of the message this allows any URL to be accessed that are included in the status update.

Using this script

To use this script you need to have a twitter account where your following other peoples twitter updates. If you have no twitter friends try your Federal politicians. You need to configure the following two variables with your twitter account details

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

Then you need to fill out the following Exchange variables in

$ExchangeServername = "servername"

Put the serverName of youy CAS server Then

$emEmailAddress = “twittest@domain.com”

Put the email address of the Exchange Mailbox where you want to create the updates.In

$userName = "twittest"
$password = "Password"
$domain = "domain"

Put a Username and password that has access to this account if you’re using a Cloud mailbox such as a microsoftonline.com account set the domain to blank and use something like

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

This script requires the latest copy of the EWSUtil which you can download from here I’ve put a download of the script here the script itself looks like.

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

$casURL = "https://" + $ExchangeServername + "/EWS/Exchange.asmx"
[void][Reflection.Assembly]::LoadFile("C:\temp\EWSUtil.dll")

$statushash = @{ }

function convertTwitterTime($ptim){
$year = $ptim.Substring(26,4)
$month = $ptim.Substring(4,3)
$day = $ptim.Substring(0,3)
$dayNum = $ptim.Substring(8,2)
$time = $ptim.Substring(11,8)
$combTime = $day + " " + $dayNum + " " + $month + " " + $year + " " + $time + " GMT"
$convertedTime = [DateTime]::Parse($combTime)
return $convertedTime.ToLocalTime()
}


$TempDir = "c:\Temp"
if (!(Test-Path -path $TempDir))
{
New-Item $TempDir -type directory
}
$StatusFile = $TempDir + "\twitStatus.xml"
if (!(Test-Path -path $StatusFile))
{
"Non Status File First Run ?"
}
else{
"Status Found"
[xml]$StatusXmlDoc = Get-Content $StatusFile
foreach($status in $StatusXmlDoc.statuses.status){
$statushash.Add($status.id.ToString(),1)
}

}
$ewc = new-object EWSUtil.EWSConnection($emEmailAddress,$false, $userName, $password,$domain, $casURL)
[System.Net.ServicePointManager]::Expect100Continue = $false
$request = [System.Net.WebRequest]::Create("http://twitter.com/statuses/friends_timeline.xml")
$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"))
for($snodes=0;$snodes -lt $StatusNodes.Length;$snodes++){
if ($statushash.Containskey($StatusNodes[$snodes].id) -eq $false){
if ($twitterusername -ne $StatusNodes[$snodes].user.screen_name){
[EWSUtil.EWS.DistinguishedFolderIdType] $dType = new-object EWSUtil.EWS.DistinguishedFolderIdType
$dType.Id = [EWSUtil.EWS.DistinguishedFolderIdNameType]::inbox
$time = convertTwitterTime($StatusNodes[$snodes].created_at)
$ewc.CreateTwitMail($dType,$time,$StatusNodes[$snodes].user.Name.Replace(" ","") + "@twitterexdev.com",$StatusNodes[$snodes].user.Name,$StatusNodes[$snodes].text)
}
}
}
$ResponseXmlDoc.Save($TempDir + "\twitStatus.xml")