Friday, January 25, 2008

Setting and Understanding Folder Permissions in Exchange Web Services

One of the more challenging things you may want to do when writing code that is going to run against an Exchange 2007 mailbox is to set and modify Folder permissions on a mailbox folder or a public folder. I’d thought I’d share my efforts and what I learnt over the last week of trying to do this (thanks to David Claux for helping me get over my issues with custom permissions). One of the features added to SP1 in Exchange 2007 is the ability to set folder permissions and also folder delegates. This is a pretty cool feature but one you do need to make sure you understand before you dive in. Before you start its a good idea to understand the permissions your want to modify to ensure your code is going to work as expected and avoid any unwanted ACL changes.

Let’s start by looking at the permissions that can be set on a folder via EWS

CanCreateItems
ReadItems
DeleteItems
EditItems
CanCreateSubFolders
IsFolderContact
IsFolderOwner
IsFolderVisible

With Exchange 2007/Outlook2007 there where Permissions added to support the new freebusy detail features in Outlook and Exchange. There’s some good information about these on Stephen Griffin's Blog . These new permissions are only valid for a calendar folder. Overlaying these based folder permissions are the Roles that a user would generally assign in Outlook. Now these Roles are just certain combinations of the above Permissions. The following are the roles you would normally see set on a folder if you’re looking in Outlook

Author
Contributor
Custom
Editor
None
NoneditingAuthor
Owner
PublishingAuthor
PublishingEditor
Reviewer

On a calendar folder you have the following additional roles to support freebusy detail

FreeBusyTimeAndSubjectAndLocation
FreeBusyTimeOnly

Now let’s look at an example of setting the calendar folder permissions using EWS. Generally when you are setting permissions you’ll be modifying the permissions on an already existing Exchange Store object. So you will be either adding an additional Access Control Entry to the permissions list or modifying the rights or an existing ACE.

In EWS calendar folder permissions are represented by the CalendarPermissionSetType object so to modify the permissions on an existing folder you need to modify the Permission Set property using an updateitem operation. Sounds easy right well this is where the complications begin.

When you use an UpdateItem operation to update a property on a folder that property you update will overwrite the existing store property. Because the whole PermissionSet is represented as one property you can’t just write the changes with an UpdateItem operation. If you do this you will end up deleting all your existing ACE’s and end up only with the changes you’re trying to make. So to cater for this you first need to get the existing CalendarPermissionSet using a GetFolder operation, you then need to create a new CalendarPermissionSet that contains all the ACE’s from the existing CalendarPermissionSet with whatever modifications you want and then post this new CalendarPermissionSet to overwrite the existing set. Now here’s a precautionary tale don’t do what I tried to which was to try and just make changes to the existing CalendarPermissionSet object you retrieve with GetFolder operation and then post this back.

The reason this could fail is also a little complicated but there are a few important points to understand.

When you include a calendarPermission in a CalendarPermissionSet you have to set the CalendarPermissionLevel. These Levels relate to the Outlook Roles I mentioned previously if these roles are set to anything other than Custom you must make sure you don’t include setting any of the base rights. So basically you can set something like the CalendarPermissionLevel to PublishingAuthor or you can set the CalendarPermissionLevel to custom and then set each of the base rights like CanCreateItems,ReadItems etc. If you try to set CalendarPermissionLevel to PublishingAuthor and also set the base rights (including the foldercontact and folderowner setting) EWS will reject the changes you’re trying to make as invalid.

Now when you get the permissionset using a GetFolder operation EWS will return a fully populated CalendarPermission object for each ACE in the Set even if the CalendarPermissionLevel isn’t set to custom. So if you try to use one of these calendarPermissions that had a CalendarPermissionLevel set to something other than custom it will cause your code to error out because it breaks the rule I mentioned in the last paragraph.

So from a coding perspective to make this work you should build a new CalendarPermissionLevel object based on the existing object with some logic to verify the CalendarPermission your including in the set is going to be valid. The logic I came up with was you can copy any of the existing custom CalendarPermissionLevel objects okay into the new CalendarPermissionSet object but for any other role you need to create a new CalendarPermission object and just copy the userid and CalendarPermissionLevel from the existing CalendarPermission.

The one thing to be careful of if you are setting Public Folder permission with EWS and you have set custom folder contacts is you may in some situations lose your custom contact folder setting if you set permissions using EWS and you don’t use the Custom Level.

So to put this all together in a code sample the following piece of code will change the default permissions on a user’s calendar from none to editor. I’ve put a download of this code here the code itself looks like.

static void Main(string[] args)
{
// Create the binding and set the credentials
ExchangeServiceBinding esb = new ExchangeServiceBinding();
esb.RequestServerVersionValue = new RequestServerVersion();
esb.RequestServerVersionValue.Version = ExchangeVersionType.Exchange2007_SP1;
ServicePointManager.ServerCertificateValidationCallback =
delegate(Object obj, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors)
{
// Replace this line with code to validate server certificate.
return true;
};

esb.Credentials = new NetworkCredential("username", "password", "domain");
esb.Url = @"https://servername/EWS/Exchange.asmx";
setcalperm(esb);

}
static void setcalperm(ExchangeServiceBinding esb)
{

DistinguishedFolderIdType cfCurrentCalendar = new DistinguishedFolderIdType();
cfCurrentCalendar.Id = DistinguishedFolderIdNameType.calendar;

FolderResponseShapeType frFolderRShape = new FolderResponseShapeType();
frFolderRShape.BaseShape = DefaultShapeNamesType.AllProperties;

GetFolderType gfRequest = new GetFolderType();
gfRequest.FolderIds = new BaseFolderIdType[1] { cfCurrentCalendar };
gfRequest.FolderShape = frFolderRShape;


GetFolderResponseType gfGetFolderResponse = esb.GetFolder(gfRequest);
CalendarFolderType cfCurrentFolder = null;
if (gfGetFolderResponse.ResponseMessages.Items[0].ResponseClass == ResponseClassType.Success)
{

cfCurrentFolder = (CalendarFolderType)((FolderInfoResponseMessageType)gfGetFolderResponse.ResponseMessages.Items[0]).Folders[0];

}
else
{//handle error
}

UserIdType auAceUser = new UserIdType();
auAceUser.DistinguishedUserSpecified = true;
auAceUser.DistinguishedUser = DistinguishedUserType.Default;

CalendarPermissionSetType cfCurrentCalPermsionsSet = cfCurrentFolder.PermissionSet;
CalendarPermissionSetType cfNewCalPermsionsSet = new CalendarPermissionSetType();
cfNewCalPermsionsSet.CalendarPermissions = new CalendarPermissionType[cfCurrentCalPermsionsSet.CalendarPermissions.Length] ;
for(int cpint=0;cpint < cfCurrentCalPermsionsSet.CalendarPermissions.Length;cpint++){
if (cfCurrentCalPermsionsSet.CalendarPermissions[cpint].UserId.SID == auAceUser.SID)
{
cfNewCalPermsionsSet.CalendarPermissions[cpint] = new CalendarPermissionType();
cfNewCalPermsionsSet.CalendarPermissions[cpint].UserId = cfCurrentCalPermsionsSet.CalendarPermissions[cpint].UserId;
cfNewCalPermsionsSet.CalendarPermissions[cpint].CalendarPermissionLevel = CalendarPermissionLevelType.Reviewer;
}
else
{
//Copy old ACE
if (cfCurrentCalPermsionsSet.CalendarPermissions[cpint].CalendarPermissionLevel == CalendarPermissionLevelType.Custom)
{
cfNewCalPermsionsSet.CalendarPermissions[cpint] = cfCurrentCalPermsionsSet.CalendarPermissions[cpint];
}
else
{
cfNewCalPermsionsSet.CalendarPermissions[cpint] = new CalendarPermissionType();
{
cfNewCalPermsionsSet.CalendarPermissions[cpint].UserId = cfCurrentCalPermsionsSet.CalendarPermissions[cpint].UserId;
cfNewCalPermsionsSet.CalendarPermissions[cpint].CalendarPermissionLevel = cfCurrentCalPermsionsSet.CalendarPermissions[cpint].CalendarPermissionLevel;
}
}
}

}


CalendarFolderType cfUpdateCalFolder = new CalendarFolderType();
cfUpdateCalFolder.PermissionSet = cfNewCalPermsionsSet;

UpdateFolderType upUpdateFolderRequest = new UpdateFolderType();

FolderChangeType fcFolderchanges = new FolderChangeType();

FolderIdType cfFolderid = new FolderIdType();
cfFolderid.Id = cfCurrentFolder.FolderId.Id;
cfFolderid.ChangeKey = cfCurrentFolder.FolderId.ChangeKey;

fcFolderchanges.Item = cfFolderid;

SetFolderFieldType cpCalPerms = new SetFolderFieldType();
PathToUnindexedFieldType cpFieldURI = new PathToUnindexedFieldType();
cpFieldURI.FieldURI = UnindexedFieldURIType.folderPermissionSet;
cpCalPerms.Item = cpFieldURI;
cpCalPerms.Item1 = cfUpdateCalFolder;

fcFolderchanges.Updates = new FolderChangeDescriptionType[1] { cpCalPerms };
upUpdateFolderRequest.FolderChanges = new FolderChangeType[1] { fcFolderchanges };

UpdateFolderResponseType ufUpdateFolderResponse = esb.UpdateFolder(upUpdateFolderRequest);
if (ufUpdateFolderResponse.ResponseMessages.Items[0].ResponseClass == ResponseClassType.Success)
{
Console.WriteLine("Permissions Updated sucessfully");
}
else
{
// Handle Error

}

}

Friday, January 18, 2008

Making use of Autodiscovery in VBS and Powershell Scripts

Auto discovery is one of the new features that are trying to alleviate some of the pain of configuring and maintaining Outlook client setting. Once you have managed to overcome the SSL configuration peculiarities you have a nifty little service that you can authenticate against, supply it with your email address and it will inform you about everything you need to know about connecting to exchange using Outlook, OWA, EWS and or any of the other new services Exchange 2007 provides. If you’re a scripter the information the auto discovery service provides can be retrieved via Active Directory but it not that easy and certain not as convenient as doing with one request.

The scripts I’ll talk about in this post are designed to be run from workstation where the Exchange Management Tools aren’t install so they won’t be using any of the Exchange Management Shell cmdlets.

The first thing you need to do if you want to use Auto discovery is query Active Directory for the Service Connection Point. This will give you the URL which you can send your AutoDiscovery Query (okay there is another option which is using autodisovery.domain.com). The SCP for Autodiscovery is located under the server/procotol nodes in the Active directory configuration partition. So to start with you need to use an ADSI query with the following filter (&(objectClass=serviceConnectionPoint) (|(keywords=67661d7F-8FC4-4fa7-BFAC-E1D7794C1F68) (keywords=77378F46-2C66-4aa9-A6A6-3E7A48B19596))). This will return one or more SCP you can then use to make a auto discovery request. If you want to find the closest SCP to the machine that is making the query you can use the Keywords attribute on the ServiceConnectionPoint to return the site name. You could also use a IP table with some metrics to work this out. Once you have the URL for auto discovery you can then send a XML request to the autodiscover service. Of course you will to also authenticate for this to work in a script you can do this in one of two way by hardcoding a username and password into the script file or by using the logged on user credentials the script from the post both make use of the Logged on user credentials.

Thats pretty much it for more information on Auto discovery I would recommend having a read of this if you want to write something in C# that uses autodiscover then check out the Auto discover sample in the Exchange SDK.

I’ve put a download of the scripts here the vbs sample looks like

ScpUrlGuidString = "77378F46-2C66-4aa9-A6A6-3E7A48B19596"
ScpPtrGuidString = "67661d7F-8FC4-4fa7-BFAC-E1D7794C1F68"

set conn = createobject("ADODB.Connection")
set com = createobject("ADODB.Command")
Set iAdRootDSE = GetObject("LDAP://RootDSE")
strNameingContext = iAdRootDSE.Get("configurationNamingContext")
Conn.Provider = "ADsDSOObject"
Conn.Open "ADs Provider"
svcQuery = "<LDAP://CN=Microsoft Exchange,CN=Services," & strNameingContext &
">;(&(objectClass=serviceConnectionPoint)" _
& "(|(keywords=" & ScpPtrGuidString & ")(keywords=" & ScpUrlGuidString & ")));cn,name,serviceBindingInformation,legacyExchangeDN;subtree"
Com.ActiveConnection = Conn
Com.CommandText = svcQuery
Set Rs = Com.Execute
while not rs.eof
wscript.echo rs.fields("cn")
call queryautodiscovery(wscript.arguments(0),rs.fields("serviceBindingInformation").Value)
rs.movenext
wend

sub queryautodiscovery(emailaddress,casAddress)
wscript.echo "Using AutoDisover Address : " & casAddress(0)
autodiscoResponse = "<Autodiscover xmlns=""http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006"">"
_
& " <Request>" _
& " <EMailAddress>" + emailaddress + "</EMailAddress>" _
& " <AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>"
_
& " </Request>" _
& "</Autodiscover>"
set req = createobject("MSXML2.ServerXMLHTTP.6.0")
req.Open "Post",casAddress(0) ,False
req.SetOption 2, 13056
req.setRequestHeader "Content-Type", "text/xml"
req.setRequestHeader "Content-Length", len(autodiscoResponse)
req.send autodiscoResponse
wscript.echo req.responsetext
end sub

Tuesday, January 15, 2008

Exporting the OOF (Out of Office) Setting for every user on a server using CDO 1.2 and VBS

This is another one that came out of the Ether the Out of office functionality was one of the things that received a welcome makeover with Exchange 2007 / Outlook 2007. If you’re moving, migrating or just wondering if people are actually using the Out of Office Assistant then this might be the script for you. This script is broken down into 2 parts the first part of the script takes one command line parameter which is the name of the server you want to export the information from and then it does a ADSI query of Active Directory to get the detail of every account on the server in question. The second part of the script connects to each of the mailboxes the ADSI query found and looks to see if the oof setting is enabled. One of the good things about CDO 1.2 is that it makes getting and setting the oof setting on a mailbox very simple basically once you have logged on you can pull the oof setting from the outofoffice and outofofficetext property of the session object. The format this script uses to export the oofsetting is XML the text of the OOF message is held in a Cdata section to allow for any non standard characters that were used in the oof message that might throw the format.

Exporting the OOF setting is great but if you want to do more than just analyze the data you get back you need something that will read the data you exported and reset them on a mailbox. With the increased functionality of the OOF in Exchange 2007 has come extra complexity so while you could reset the OOF setting on Exchange 2007 using CDO 1.2 to make use of the extra functionality like internal and external OOF message you need to use Exchange Web Service. So watch this space for a sample.

To run the script you need to give it one cmdline parameter which is the servername of the server you want to run it against. Eg cscript exportoof.vbs servername it will write a file to c:\temp\offexport-servername.xml

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

servername = wscript.arguments(0)
Set fso = CreateObject("Scripting.FileSystemObject")
set wfile = fso.opentextfile("c:\temp\offexport-" & servername & ".xml",2,true)

wfile.writeline("<?xml version=""1.0""?>")
wfile.writeline("<ExportedOffs ExportDate=""" & WeekdayName(weekday(now),3) & ",
" & day(now()) & " " & Monthname(month(now()),3) & " " & year(now()) & " " &
formatdatetime(now(),4) & ":00" & """>")
set conn = createobject("ADODB.Connection")
set com = createobject("ADODB.Command")
Set iAdRootDSE = GetObject("LDAP://RootDSE")
strNameingContext = iAdRootDSE.Get("configurationNamingContext")
strDefaultNamingContext = iAdRootDSE.Get("defaultNamingContext")
Conn.Provider = "ADsDSOObject"
Conn.Open "ADs Provider"
svcQuery = "<LDAP://" & strNameingContext & ">;(&(objectCategory=msExchExchangeServer)(cn="
& Servername & "));cn,name,legacyExchangeDN;subtree"
Com.ActiveConnection = Conn
Com.CommandText = svcQuery
Set Rs = Com.Execute
while not rs.eof
GALQueryFilter = "(&(&(&(& (mailnickname=*)(!msExchHideFromAddressLists=TRUE)(|
(&(objectCategory=person)(objectClass=user)(msExchHomeServerName=" &
rs.fields("legacyExchangeDN") & ")) )))))"
strQuery = "<LDAP://" & strDefaultNamingContext & ">;" & GALQueryFilter & ";distinguishedName,mailnickname,mail;subtree"
com.Properties("Page Size") = 100
Com.CommandText = strQuery
Set Rs1 = Com.Execute
while not Rs1.eof
call procmailboxes(servername,rs1.fields("mail"))
wscript.echo rs1.fields("mail")
rs1.movenext
wend
rs.movenext
wend
rs.close
wfile.writeline("</ExportedOffs>")
wfile.close
set fso = nothing
set conn = nothing
set com = Nothing

wscript.echo "Done"




sub procmailboxes(servername,MailboxAlias)

Set msMapiSession = CreateObject("MAPI.Session")
on error Resume next
msMapiSession.Logon "","",False,True,True,True,Servername & vbLF & MailboxAlias
if err.number = 0 then
on error goto 0
if msMapiSession.outofoffice = false and msMapiSession.outofofficetext = "" then
wscript.echo "No OOF Data for user"
else
wfile.writeline("<OOFSetting DisplayName=""" & msMapiSession.CurrentUser & """
EmailAddress=""" & MailboxAlias & """ Offset=""" _
& msMapiSession.outofoffice & """><![CDATA[ " & msMapiSession.outofofficetext & "]]></OOFSetting>")
End if
else
Wscript.echo "Error Opening Mailbox"
end if
Set msMapiSession = Nothing
Set mrMailboxRules = Nothing

End Sub

Monday, January 14, 2008

Finding the number of Unread Voicemail Messages in a Mailbox

One thing that you can be sure to expect to see more in the future is richer types of messages arriving in your inbox one example of this is Voice Mail Messages in Exchange 2007. The interesting thing about this is the way this changes the dynamics of a simple thing like the unread message count in your mailbox. For example how do you now balance the priority of a voice message to that of email message eg which one do you read first (or listen to first). How do you weigh the contents of a voice message to that of a text message and if your away for a longer period of time and amass a larger number of voice and email message in your mailbox can you really get away with just marking them all as read.

Okay maybe I can’t reengineer the space time continuum with this post to solve this but I can show a few ways of show how many unread voicemails you have in a mailbox.

The usually way of getting the unread message count on your inbox is just to use the unread property of the mailbox folder PR_CONTENT_UREAD . Because of the rich data types in the mailbox this property isn’t going to give you the detail of how many unread voice mail messages are in a folder. To find this you have to Search or Filter the folder in question depending on the API your using. For Example in CDO 1.2 you can create a filter on the Inbox to filter all the messages that are unread and have a message class of Type IPM.Note.Microsoft.Voicemail.UM.CA eg

Set msMapiSession = CreateObject("MAPI.Session")
on error Resume next
msMapiSession.Logon "","",False,True,True,True,Servername & vbLF & MailboxAlias
if err.number = 0 then
on error goto 0
set ifInboxFolderCol = msMapiSession.inbox.messages
set vmFilter = ifInboxFolderCol.Filter
vmFilter.Unread = True
Set vmFilterFiled = mFilter.Fields.Add(&h001A001E,"IPM.Note.Microsoft.Voicemail.UM.CA") Wscript.echo mailboxAlias & " " & ifInboxFolderCol.Count

Else
Wscript.echo "Error Opening Mailbox"
end if
Set msMapiSession = Nothing
Set mrMailboxRules = Nothing

With Exchange Web Services you need to use a Finditems request and apply a restriction to that request. A restriction that will limit both unread and Voice mail item would look like.

RestrictionType ffRestriction = new RestrictionType();
AndType raRestictionAnd = new AndType();
raRestictionAnd.Items = new SearchExpressionType[2];
ContainsExpressionType ceContainsVM = new ContainsExpressionType();
ceContainsVM.ContainmentComparison = ContainmentComparisonType.IgnoreCase;

ceContainsVM.ContainmentComparisonSpecified = true;
ceContainsVM.ContainmentMode = ContainmentModeType.FullString;
ceContainsVM.ContainmentModeSpecified = true;
PathToUnindexedFieldType icItemClassProperty = new PathToUnindexedFieldType();
icItemClassProperty.FieldURI = UnindexedFieldURIType.itemItemClass;
ceContainsVM.Item = icItemClassProperty;
ConstantValueType cvConstant = new ConstantValueType();
cvConstant.Value = "IPM.Note.Microsoft.Voicemail.UM.CA";
ceContainsVM.Constant = cvConstant;
IsEqualToType ieToTypeRead = new IsEqualToType();
PathToUnindexedFieldType rsReadStatus = new PathToUnindexedFieldType();
rsReadStatus.FieldURI = UnindexedFieldURIType.messageIsRead;
ieToTypeRead.Item = rsReadStatus;
FieldURIOrConstantType isReadFalse = new FieldURIOrConstantType();
isReadFalse.Item = new ConstantValueType();
(isReadFalse.Item as ConstantValueType).Value = "";
ieToTypeRead.FieldURIOrConstant = isReadFalse;
raRestictionAnd.Items[0] = ceContainsVM;
raRestictionAnd.Items[1] = ieToTypeRead;
ffRestriction.Item = raRestictionAnd;

These two examples can be used to get the number of unread voice mail for individual accounts if you wanted to do this for every UMenabled mailbox on a server you would need to use one of a few methods to find mailboxes that are UMenabled. The easiest is to use the Get-UMMailbox Exchange Management Shell cmdlet . In VBS you can use ADSI and query for mailboxes that msExchUMEnabledFlags AD property set. I’ve put together a VBS sample that will query for all mailbox on a server that are UMenabled using ADSI and then access each mailbox and find the number of unread voice mail and compile the result to text file. I’ve included this script in the download along with the C# EWS finditem sample you can download this from here.

Tuesday, January 08, 2008

Adding your own X-header when sending a message via Exchange Web Services

Thought this was worth a post its something that pretty easy to do with EWS but not something that maybe initially obvious. If you want to add an X-header to a message your sending via Exchange you need to add this via the PS_INTERNET_HEADERS namespace. With EWS you do this when using the MessageType class by adding an Extended property. Eg if you take the basic send message via EWS example from MSDN http://msdn2.microsoft.com/en-us/library/bb408521.aspx and all you need to do is add the following to add your own custom X-header


message.Sensitivity = SensitivityChoicesType.Normal;

// Start X-header Code

PathToExtendedFieldType epExPath1 = new PathToExtendedFieldType();
epExPath1.DistinguishedPropertySetId = DistinguishedPropertySetType.InternetHeaders;
epExPath1.DistinguishedPropertySetIdSpecified = true;
epExPath1.PropertyName = "x-myheader";
epExPath1.PropertyType = MapiPropertyTypeType.String;

ExtendedPropertyType xhXheaderProp = new ExtendedPropertyType();
xhXheaderProp.ExtendedFieldURI = epExPath1;
xhXheaderProp.Item = "blah";
message.ExtendedProperty = new ExtendedPropertyType[] { xhXheaderProp };

// End X-header Code

// Add the message to the array of items to be created.
createItemRequest.Items.Items = new ItemType[1];