InfiniTec - Henning Krauses Blog

Don't adjust your mind - it's reality that is malfunctioning

More on meetings and meeting requests

Updates

  • 2007-12-13 21:20 GMT: Updated the article to include information about Outlook Web Access
  • 2008-01-20 12:00 GMT: Important Bugfix.

In this article, I wrote about the global object Id Outlook uses to correlate meetings and meeting requests. I also wrote that this value is set by Exchange on one of two properties: The global object id (http://schemas.microsoft.com/mapi/id/{6ED8DA90-450B-101B-98DA-00AA003F1305}/0x3). It does, however, not set the other object id, which is used by Outlook in cached mode: http://schemas.microsoft.com/mapi/id/{6ED8DA90-450B-101B-98DA-00AA003F1305}/0x23.

Because of this, I gave the advise to save the meeting to the calendar folder and then reload the meeting along with the autogenerated global object id. Finally, set the second id to the value of the first and commit the changed to the server.

It turns out that this method is not only inefficient but also unnecessary.

You can just set both id values to an arbitrary value. For compatibility reasons, this should be of type binary. Because this id should be unique across all mailboxes on a server, a global unique identifier (GUID) should be used. You can use this snippet to create a base64 encoded string which can be used as a global object id:

    1 Guid id;

    2 string value;

    3 

    4 id = Guid.NewGuid();

    5 value = Convert.ToBase64String(id.ToByteArray());

This solution works fine for all MAPI clients. But it won't work with Outlook Web Access, because OWA uses another property to identify meetings: urn:schemas:calendar:uid. You can set this property to the same value as the global object id - just save it with the datatype string. Here is an example for a meeting created with my library. I have cleaned up the namespaces to improve readability.

[UPDATE 2008-01-20 12:00 GMT]

You have to set the property 0x8218 in the appointment propertyset (http://schemas.microsoft.com/mapi/id/{00062002-0000-0000-C000-000000000046}/0x8218) to 1. This is the ResponseType property and a value of 1 means Meeting Organizer. If you don't set this property, Outlook will remove this appointment after you get a meeting response from one of the attendees.

    1 PROPPATCH /exchange/administrator/Calendar/08986b34-384a-4e39-88b3-2b188fe2f349.eml HTTP/1.1

    2 Depth: 0

    3 Content-Type: text/xml

    4 Accept-Encoding: gzip, deflate

    5 Authorization: Negotiate YIIKlgYGKwYBBQUCoIIKijCCCoagJDAiBgkqhkiC9

    6 Host: w2k3srv.contoso.local

    7 Content-Length: 2822

    8 Expect: 100-continue

    9 

   10 <?xmlversion="1.0"encoding="UTF-8"?>

   11 <propertyupdate

   12     xmlns:dt="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/"

   13     xmlns="DAV:"

   14     xmlns:common="http://schemas.microsoft.com/mapi/id/{00062008-0000-0000-c000-000000000046}/"

   15     xmlns:proptag="http://schemas.microsoft.com/mapi/proptag/"

   16     xmlns:cal="http://schemas.microsoft.com/mapi/id/{00062008-0000-0000-c000-000000000046}/"

   17     xmlns:meeting="http://schemas.microsoft.com/mapi/id/{6ed8da90-450b-101b-98da-00aa003f1305}/">

   18     <set>

   19         <prop>

   20             <proptag:x001a001fdt:dt="string">IPM.Appointment</proptag:x001a001f>

   21             <contentclassdt:dt="string">urn:content-classes:appointment</contentclass>

   22             <proptag:x0037001fdt:dt="string">test5</proptag:x0037001f>

   23             <cal:_x0030_x820ddt:dt="dateTime.tz" >2007-12-13T23:57:48Z</cal:_x0030_x820d>

   24             <proptag:x00600040dt:dt="dateTime.tz">2007-12-13T23:57:48Z</proptag:x00600040>

   25             <cal:_x0030_x8516dt:dt="dateTime.tz">2007-12-13T23:57:48Z</cal:_x0030_x8516>

   26             <cal:_x0030_x8215dt:dt="boolean" >0</cal:_x0030_x8215>

   27             <common:_x0030_x8502dt:dt="dateTime.tz">2007-12-13T23:57:48Z</common:_x0030_x8502>

   28             <common:_x0030_x8503dt:dt="boolean">1</common:_x0030_x8503>

   29             <cal:_x0030_x820edt:dt="dateTime.tz" >2007-12-14T01:57:48Z</cal:_x0030_x820e>

   30             <proptag:x00610040dt:dt="dateTime.tz">2007-12-14T01:57:48Z</proptag:x00610040>

   31             <cal:_x0030_x8517dt:dt="dateTime.tz">2007-12-14T01:57:48Z</cal:_x0030_x8517>

   32             <cal:_x0030_x8205dt:dt="int" >2</cal:_x0030_x8205>

   33             <cal:_x0030_x8223dt:dt="boolean" >0</cal:_x0030_x8223>

   34             <cal:_x0030_x8217dt:dt="int" >1</cal:_x0030_x8217>

   35             <cal:_x0030_x8218dt:dt="int" >1</cal:_x0030_x8218>

   36             <meeting:_x0030_x23dt:dt="bin.base64">HKSFpjzfdEK75L/tHWI4rQ==</meeting:_x0030_x23>

   37             <meeting:_x0030_x3dt:dt="bin.base64">HKSFpjzfdEK75L/tHWI4rQ==</meeting:_x0030_x3>

   38             <uiddt:dt="string"xmlns="urn:schemas:calendar:">a685a41c-df3c-4274-bbe4-bfed1d6238ad</uid>

   39             <todt:dt="string"xmlns="urn:schemas:mailheader:">"Alice" &lt;alice@contoso.local&gt;</to>

   40         </prop>

   41     </set>

   42 </propertyupdate>

The corresponding meeting request looks like this:

    1 PROPPATCH /exchange/administrator/Drafts/18e81af8-cfe9-4abb-86bf-814b277cb0bf.eml HTTP/1.1

    2 Depth: 0

    3 Content-Type: text/xml

    4 Accept-Encoding: gzip, deflate

    5 Authorization: Negotiate YIIKlgYGKwYBBQUCoIIKijCCCoagJDAi

    6 Host: w2k3srv.contoso.local

    7 Content-Length: 2766

    8 Expect: 100-continue

    9 

   10 <?xmlversion="1.0"encoding="UTF-8"?>

   11 <propertyupdatexmlns:dt="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/"

   12                 xmlns:proptag="http://schemas.microsoft.com/mapi/proptag/"

   13                 xmlns:meeting="http://schemas.microsoft.com/mapi/id/{6ed8da90-450b-101b-98da-00aa003f1305}/"

   14                 xmlns:appointment="http://schemas.microsoft.com/mapi/id/{00062002-0000-0000-c000-000000000046}/"

   15                 xmlns:common="http://schemas.microsoft.com/mapi/id/{00062008-0000-0000-c000-000000000046}/"

   16                 xmlns="DAV:">

   17     <set>

   18         <prop>

   19             <proptag:x001a001fdt:dt="string" >IPM.Schedule.Meeting.Request</proptag:x001a001f>

   20             <contentclassdt:dt="string">urn:content-classes:calendarmessage</contentclass>

   21             <proptag:x0037001fdt:dt="string" >test5</proptag:x0037001f>

   22             <appointment:_x0030_x820ddt:dt="dateTime.tz" >2007-12-13T23:57:48Z</appointment:_x0030_x820d>

   23             <proptag:x00600040dt:dt="dateTime.tz" >2007-12-13T23:57:48Z</proptag:x00600040>

   24             <common:_x0030_x8516dt:dt="dateTime.tz">2007-12-13T23:57:48Z</common:_x0030_x8516>

   25             <appointment:_x0030_x820edt:dt="dateTime.tz" >2007-12-14T01:57:48Z</appointment:_x0030_x820e>

   26             <proptag:x00610040dt:dt="dateTime.tz" >2007-12-14T01:57:48Z</proptag:x00610040>

   27             <common:_x0030_x8517dt:dt="dateTime.tz">2007-12-14T01:57:48Z</common:_x0030_x8517>

   28             <appointment:_x0030_x8215dt:dt="boolean" >0</appointment:_x0030_x8215>

   29             <appointment:_x0030_x8223dt:dt="boolean" >0</appointment:_x0030_x8223>

   30             <meeting:_x0030_x3dt:dt="bin.base64">HKSFpjzfdEK75L/tHWI4rQ==</meeting:_x0030_x3>

   31             <meeting:_x0030_x23dt:dt="bin.base64" >HKSFpjzfdEK75L/tHWI4rQ==</meeting:_x0030_x23>

   32             <uiddt:dt="string"xmlns="urn:schemas:calendar:">a685a41c-df3c-4274-bbe4-bfed1d6238ad</uid>

   33             <appointment:_x0030_x8217dt:dt="int" >3</appointment:_x0030_x8217>

   34             <appointment:_x0030_x8205dt:dt="int" >2</appointment:_x0030_x8205>

   35             <appointment:_x0030_x8224dt:dt="int" >2</appointment:_x0030_x8224>

   36             <proptag:x0063000bdt:dt="boolean" >1</proptag:x0063000b>

   37             <todt:dt="string"xmlns="urn:schemas:mailheader:">"Alice" &lt;alice@contoso.local&gt;</to>

   38         </prop>

   39     </set>

   40 </propertyupdate>

 


Technorati:

Posted by Henning Krause on Friday, July 13, 2007 12:00 AM, last modified on Saturday, July 2, 2011 3:51 PM
Permalink | Post RSSRSS comment feed

RSS feed registrations in Outlook 2007 disappear - the solution

If you have many RSS feed registrations in Outlook 2007, you may have noticed that at some point newly added RSS feeds start disappearing. They are added successfully, but on the next restart of Outlook, the registration is just gone.

You can work around this by setting the registry key DisableRoaming in HKEY_CURRENT_USER\Software\Microsoft\Office\12.0\Outlook\RSS to 1. If this key is not yet present, create it with a type of DWORD.

You'll have to restart Outlook for the change to take effect.

But be cautious: I don't know which side-effects this registry might have.


Technorati:

Posted by Henning Krause on Friday, July 13, 2007 12:00 AM, last modified on Friday, July 13, 2007 12:00 PM
Permalink | Post RSSRSS comment feed

Getting the original values of an item in an OnSyncSave event sink

If you are developing an OnSyncEvent sink (see here for an article on how to implement them with managed code), you may need the value of the item being modified before the event took place. Unfortunately, the Eventsink infrastructure only exposes the modified item.

The OnSyncSave event sink is called twice: Before and after the changes have been committed to to the store. In the first call, you can get the original values simply by opening the modified item again.

Let's assume you got the ADO Record instance from the supplied pEventInfo parameter with this code:

    1 IExStoreDispEventInfo eventItem;

    2 eventItem = (IExStoreDispEventInfo) pEventInfo;

    3 Record record;

    4 record = (Record) eventItem.EventRecord;

Once you have this record, you can get the permanent url of the item, create a new ADO Record instance and load the original item into that record:

    1 try

    2 {

    3     string url;

    4 

    5     url = (string)record.Fields["http://schemas.microsoft.com/exchange/permanenturl"].Value;

    6 

    7     _TraceSource.TraceEvent(TraceEventType.Verbose, 0, "Opening original item at " + url);

    8 

    9     original = newRecord();

   10     original.Open(url, eventItem.EventConnection, ConnectModeEnum.adModeRead,

   11                   RecordCreateOptionsEnum.adFailIfNotExists, RecordOpenOptionsEnum.adOpenSource, null,

   12                   null);

   13 }

   14 catch (COMException ex)

   15 {

   16     if ((uint) ex.ErrorCode == 0x80040e19)

   17     {

   18         _TraceSource.TraceEvent(TraceEventType.Verbose, 0, Resources.CannotOpenOriginalItem);

   19         return;

   20     }

   21     else

   22     {

   23         throw;

   24     }

   25 }

You can now access the original item and access its properties.


Technorati:

Posted by Henning Krause on Thursday, July 5, 2007 12:00 AM, last modified on Thursday, July 5, 2007 12:00 PM
Permalink | Post RSSRSS comment feed

Creating meeting requests with WebDAV and Outlook in Cached mode

Under the support article KB308373, Microsoft had an example on how to create a meeting request for a meeting with WebDAV. Unfortunately, they have removed this article. Bascially, the process is like this:

  1. Create a meeting item in the calendar folder
  2. Create a messge item with the Outlook Message class of IPM.Schedule.Meeting.Request and a DAV:contentclass of urn:content-classes:calendarmessage. Then, copy all appointment related properties from the appointment to the meeting request item.
  3. Save the request message to the drafts folder of your mailbox.
  4. Move the message from the drafts folder to the ##MailSubmissionUri## folder.

This works in principle - unless you are using Outlook in cached mode. In this case, Outlook won't find the appointment which corresponds to the meeting request. Depending whether Outlook is in online or cached mode, it uses two different properties to find the appointment connected to a request: The global object id (http://schemas.microsoft.com/mapi/id/{6ED8DA90-450B-101B-98DA-00AA003F1305}/0x3) and a cached version of the global object id (http://schemas.microsoft.com/mapi/id/{6ED8DA90-450B-101B-98DA-00AA003F1305}/0x23). More information on the global object id can be found here. The global object id is set by Exchange. The other object id seems to be created by Outlook only.

So, if you create a meeting request via WebDAV and don't set the second object id, Outlook in cached mode will not find your appointment entry.

One solution to this problem is to reload the appointment after step 1 and load the global object id. Then, set the second global object id to the value of the first. And add the global object id to the properties of the meeting request.


Technorati:

Posted by Henning Krause on Monday, July 2, 2007 12:00 AM, last modified on Monday, February 28, 2011 10:21 AM
Permalink | Post RSSRSS comment feed

Displaying progress updates when hashing large files

To hash a file (either using MD5 or SHA1), you can use the corresponding classes in the .NET Framework (MD5 or SHA1) with only a few lines of code:

    1 using (Stream stream = File.OpenRead(filename)

    2 using (MD5 md5 = MD5.Create())

    3 {

    4     md5.ComputeHash(stream);

    5    // Hash can be accessed through md5.HashValue

    6 }

This will compute the hash for the specified filename. But if your file is very large, it would be nice to have a progress bar. But ComputeHash does not implement some sort of callback mechanisms to report progress.

But the HashAlgorithm class (which all hash algorithms in the .NET Framework derive from) has two other methods: TranformBlock and TransformFinalBlock. These methods make it possible to compute the hash from discrete blocks of data. For every block other than the last, call TransformBlock. For the last block, call TransformFinalBlock. The resulting hash is now accessible via the HashValue property.

Here is a sample implementation using a background worker:

    1 privatevoid BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)

    2 {

    3     byte[] buffer;

    4     byte[] oldBuffer;

    5     int bytesRead;

    6     int oldBytesRead;

    7     long size;

    8     long totalBytesRead = 0;

    9 

   10     using (Stream stream = File.OpenRead((string) e.Argument))

   11     using (HashAlgorithm hashAlgorithm = MD5.Create())

   12     {

   13         size = stream.Length;

   14 

   15         buffer = newbyte[4096];

   16 

   17         bytesRead = stream.Read(buffer, 0, buffer.Length);

   18         totalBytesRead += bytesRead;

   19 

   20         do

   21         {

   22             oldBytesRead = bytesRead;

   23             oldBuffer = buffer;

   24 

   25             buffer = newbyte[4096];

   26             bytesRead = stream.Read(buffer, 0, buffer.Length);

   27 

   28             totalBytesRead += bytesRead;

   29 

   30             if (bytesRead == 0)

   31             {

   32                 hashAlgorithm.TransformFinalBlock(oldBuffer, 0, oldBytesRead);

   33             }

   34             else

   35             {

   36                 hashAlgorithm.TransformBlock(oldBuffer, 0, oldBytesRead, oldBuffer, 0);

   37             }

   38 

   39             BackgroundWorker.ReportProgress((int)​((double)totalBytesRead * 100 / size));

   40         } while (bytesRead != 0);

   41 

   42         e.Result = hashAlgorithm.Hash;

   43     }

   44 }

The full source code is attached to this article.

Downloads

Md5Creator.zip (11,121 Bytes)
MD5 Sample application source code

Technorati:

Posted by Henning Krause on Saturday, June 9, 2007 12:00 AM, last modified on Saturday, June 9, 2007 12:00 PM
Permalink | Post RSSRSS comment feed

InfiniTec.Exchange 0.99.4 released

This release brings a bunch of bugfixes and some new features: The Subscription and SubscriptionCollection classes implement the WebDAV push event feature: Now you can subscribe to changes made to folders and elements. Here's a diagram of the system:


Class diagram of the Subscription management (click to enlarge)

Access to the SubscriptionCollection is exposed via the Connection Class, although you are free to create your own instance. By default, it binds to a local port between 1024 and 5000, but you can specify a fixed port also. An example is not yed available.

Another cool new feature is the incorporation of my little Expression classes. These classes allow you to easily build expressions for filter items in a ItemCollection or Searcher class. A typical example would be to filter all appointments in a folder between a specified date range. You can do this now very easily:

    1     Expression expression = Expression.And(Expression.Equals(WellknownProperties.Appointment.Starttime, DateTime.Now),

    2                                        Expression.Equals(WellknownProperties.Appointment.EndTime, DateTime.Now.AddDays(3)));

    3 

    4 using (Connection connection = newConnection(server, ConnectionProtocol.Default, newCredential(username, password, AuthenticationType.Ntlm)))

    5 {

    6     Mailbox mbx = newMailbox("exchange/administrator", connection);

    7     mbx.Refresh();

    8 

    9     mbx.Calendar.Items.ConstraintExpression += expression;

   10     mbx.Calendar.Items.Refresh();

   11 

   12     foreach (CalendarItem item in mbx.Calendar.Items)

   13     {

   14         ProcessItem(item);

   15     }

   16 }

One special thing about this: You can concatenate two expressions by using the plus operator. This will result in both expressions being ANDed.

No more worrying about casting properties to the right type and such. You don't even have to worry about time zones - all handled automagically.

Here is a small overview over the expression classes:


Class diagram for the expression classes (click to enlarge)

Changelog

  • Fixed a bug in the FolderCollection class
  • Fixed a bug in the TaskItem.PreSave() method which caused the Content class of the item not to be set.
  • Fixed a bug in the TaskItem.PreSave() method which prevented a successful modification of the TaskStatus (Would not work if PercentComplete was not set)
  • The Searcher<ItemType> now get the correct set of properties for the specific item types.
  • Changed the type of TaskItem.PercentComplete from float to double
  • Exchange 'float' datatype is now handled as 'System.Double' instead of 'System.Float'
  • New method ApplyInheritedSecurity(FolderSecurity): Clears all security descriptor entries and applies the security settings from the specified descriptor. (Experimental!)
  • SchemaClass attribute is now public
  • The ItemCollection and ItemCollection<ItemType> classes now expose a IList<string> PropertiesToLoad property
  • Renamed ChildItems and ChildFolders of the Folder and Folder<ItemType> classes to Items and Folders.
  • Moved the IsRead property from MessageItem to Item
  • Made the IsRead property writable
  • Fixed a bug in the Property class which prevented setting propertyvalues to 0 or false
  • Replaced the HttpStatus class with a PropertyStatus enumeration. HttpStatus was used with the Property.Status property.
  • Added support for InfiniTec.Expressions on the Searcher, Item- and FolderCollections. Constraints can now be either specified using the Constraints property or the ConstraintExpression property.
  • SubscriptionCollection and Subscription implement WebDAV events

Downloads

ReleaseBuild.zip (497,091 Bytes)
Optimized build with a strong name and AuthentiCode signature
SourceAndDemoApplication.zip (496,678 Bytes)
The source files and the demo application
Documentation.zip (2,087,722 Bytes)
The documentation as CHM file

Technorati:

Posted by Henning Krause on Monday, April 9, 2007 12:00 AM, last modified on Monday, April 9, 2007 12:00 PM
Permalink | Post RSSRSS comment feed

Constructing OWA 2007 item ids from WebDAV items

Prior Exchange 2007, it was easy for an application to get an url for any Exchange item which could be used to open the element in a browser: Simply use the DAV:href property which could be retrieved from any item.

When you try this with Exchange 2007, you won't get the expected result. Outlook Web Access 2007 (OWA) uses a different schema to open files. Instead of

    1 http://server/exchange/alias/contacts/test.eml

OWA 2007 uses this format:

    1 https://server/owa/?ae=Item&a=Open&t=IPM.Contact&s=Draft&id=RgAAAADns5U5Pdo%2bSamzP%2fK78yQOBwBgW74qsZ7RTJzeGdXyv4QaAAhgHichAABgW74qsZ7RTJzeGdXyv4QaAAhgHjBxAAAR

But if you query the DAV:href property via WebDAV in Exchange 2007, you still get the url as seen in the first example. The tricky part of the second url is clearly the id parameter, as everything else is either static (depending on the operation one wants to perform) or can be directly derived from the item (the message class for example).

It turns out that the id parameter is a modified version of the entry id of the element. To derive the id from the entry id, you must follow these steps:

  1. Load the entry id via WebDAV (PROPFIND or SEARCH on the item, get property http://schemas.microsoft.com/mapi/proptag/xfff0102).
  2. Decode the BASE64 encoded entry id to a byte array (in .NET, use the Convert.FromBase64String() method)
  3. Create a new byte array with the length of the entryid byte array plus 2
  4. Write the length of the entry id in the first byte of the byte array
  5. Copy the entry id to the new byte array, starting at offset 1
  6. Write the item type identifier into the last byte of the element
  7. Convert the byte array to a BAS64 encoded string (using Convert.ToBase64String() method)
  8. Urlencode the string

Here is a C# snippet which does the conversion:

    1 publicstaticstring CreateOwaUrl(string entryId, ItemType itemTypeId)

    2 {

    3     byte[] binaryEntryId;

    4     byte[] owaId;

    5     string result;

    6 

    7     binaryEntryId = Convert.FromBase64String(entryId);

    8     owaId = newbyte[binaryEntryId.Length+2];

    9     owaId[0] = (byte) binaryEntryId.Length;

   10     Array.Copy(binaryEntryId, 0, owaId, 1, binaryEntryId.Length);

   11     owaId[owaId.Length - 1] = (byte) itemTypeId;

   12 

   13     result = Convert.ToBase64String(owaId);

   14     result = System.Web.HttpUtility.UrlEncode(result);

   15 

   16     return result;

   17 }

I've discovered most of the item identifiers, but there are certainly more...

    1 publicenumItemType: byte

    2 {

    3     None = 0,

    4     Folder = 0x1,

    5     CalendarFolder = 0x2,

    6     ContactsFolder = 0x3,

    7     TasksFolder = 0x4,

    8     NotesFolder = 0x5,

    9     JournalFolder = 0x6,

   10     Message = 0x9,

   11     MeetingMessage = 0xa,

   12     CalendarItem = 0xf,

   13     CalendarItemOccurrence = 0x10,

   14     Contact = 0x11,

   15     DistributionList = 0x12,

   16     Task = 0x13,

   17     TaskRequest = 0x14,

   18     Note = 0x15,

   19     Post = 0x16,

   20 }

 


Technorati:

Posted by Henning Krause on Saturday, March 24, 2007 12:00 AM, last modified on Sunday, November 28, 2010 9:56 PM
Permalink | Post RSSRSS comment feed

Finalbuilder 5 crashing instantly when started

I got my copy of FinalBuilder a few days ago, and installed it right away. Unfortunately, it crashed the instant I ran it. Turns out, its the licensing scheme they are using. Its broken by the "Data Execution Prevention" feature in Vista which is supposed to protect one from buffer overflows. So you'll need to add the FinalBuilder executables to the exclusion list, like in this screenshot:


(click to enlarge)

Add the files ActionStudio.exe and FinalBuilder5.exe from the FinalBuilder program directory to the list.

I hope they fix this soon.


Technorati:

Posted by Henning Krause on Thursday, March 15, 2007 12:00 AM, last modified on Thursday, March 15, 2007 8:00 PM
Permalink | Post RSSRSS comment feed

Determining the type of an user account

When playing around with Microsoft Exchange security descriptors you inevitably come to the point where you must sort the security descriptors according to the following rules:

    1 REPEAT <n>

    2   GRANT ACCESS RIGHT FOR USER A

    3   DENY ACCESS RIGHT FOR USER A

    4 REPEAT <m>

    5   GRANT ACCESS RIGHT FOR DL B

    6   GRANT ACCESS RIGHT FOR DL C

    7 REPEAT <m>

    8   GRANT ACCESS DENY FOR DL B

    9   GRANT ACCESS DENY FOR DL C

   10 REPEAT <0 or 1>

   11 GRANT ACCESS RIGHT FOR EVERYONE

(This was taken from the article Exchange 5.5 Access Rights and the Exchange Store on MSDN)

So you must somehow distinguish between normal users, groups, the everyone group and the anonymous group. If you read the security descriptor as XML stream via the http://schemas.microsoft.com/exchange/security/descriptor property, you get this information for the existing entries. But if you add another principal to the list, you must insert the entry at the correct position, depending of the type of the account being added.

Unfortunately, you can get this information from the Security identifier of the entry only for a few exceptions: The Everyone group and the anonymous groups (see this KB article). To identify the all others, you have basically two options:

  • Get the corresponding entry from Active Directory and check the object class
  • Use Win32 functions LookupAccountSid and LookupAccountName, which return the type of the account

In this article, I'll present a .NET implementation for the latter approach.

The implementation

The following code is taken from my InfiniTec.Security library, and implements a class, which takes a IdentityReference instance in its constructor. The account type is then exposed via the AccountType property. Note that you can also specify a remote computer, on which the translation should be performed. Since no alternative credentials can be specified, you must impersonate another user account if needed.

    1 using System;

    2 using System.Runtime.InteropServices;

    3 using System.Security.Principal;

    4 using System.Text;

    5 

    6 ///<summary>

    7 /// Provides the ability to translate a <see cref="NTAccount"/> to a <see cref="SecurityIdentifier"/>

    8 /// and vice versa, optionally using a remote computer for the translation process.

    9 ///</summary>

   10 publicclassIdentityResolver: IEquatable<IdentityReference>

   11 {

   12     privatestring _ComputerName;

   13     privateAccountType _AccountType;

   14     privateNTAccount _Account;

   15     privateSecurityIdentifier _Sid;

   16 

   17     ///<summary>

   18     /// Returns the <see cref="SecurityIdentifier"/> of this instance.

   19     ///</summary>

   20     publicSecurityIdentifier Sid

   21     {

   22         get

   23         {

   24             if (_Sid == null) TranslateFromNTAccount();

   25             return _Sid;

   26         }

   27     }

   28     ///<summary>

   29     /// Returns the <see cref="NTAccount"/> of this instance.

   30     ///</summary>

   31     publicNTAccount Account

   32     {

   33         get

   34         {

   35             if (_Account == null) TranslateFromSecurityDescriptor();

   36             return _Account;

   37         }

   38     }

   39 

   40     ///<summary>

   41     /// Returns the <see cref="AccountType"/> of this instance

   42     ///</summary>

   43     publicAccountType AccountType

   44     {

   45         get {

   46             if (_Sid == null) TranslateFromNTAccount();

   47             elseif (_Account == null) TranslateFromSecurityDescriptor();

   48 

   49             return _AccountType;

   50         }

   51     }

   52 

   53     ///<summary>

   54     /// Creates a new instance of this class. A remote computer is not used for the

   55     /// translation process

   56     ///</summary>

   57     ///<param name="identity">The identity to translate</param>

   58     public IdentityResolver(IdentityReference identity): this(identity, null) { }

   59 

   60     ///<summary>

   61     /// Creates a new instance of this class.

   62     ///</summary>

   63     ///<param name="identity">The identity to translate</param>

   64     ///<param name="computerName">The computer to use for the translation</param>

   65     ///<remarks>The remote computer is not used for translation, if

   66     /// the provided identity is a <see cref="SecurityIdentifier"/> and the domain

   67     /// sid of that identity equals the domain sid of the current user</remarks>

   68     public IdentityResolver(IdentityReference identity, string computerName)

   69     {

   70         _Sid = identity asSecurityIdentifier;

   71         _Account = identity asNTAccount;

   72 

   73         if (_Sid == null || !_Sid.IsEqualDomainSid(WindowsIdentity.GetCurrent().User))

   74         {

   75             _ComputerName = computerName;

   76         }

   77 

   78     }

   79 

   80     privatevoid TranslateFromNTAccount()

   81     {

   82         byte[] binarySid;

   83         uint binarySidLength;

   84         StringBuilder referencedDomain;

   85         uint referencedDomainLength;

   86         bool result;

   87 

   88         binarySid = newbyte[SecurityIdentifier.MaxBinaryLength];

   89         binarySidLength = (uint) binarySid.Length;

   90         referencedDomain = newStringBuilder(0xff);

   91         referencedDomainLength = (uint) referencedDomain.Capacity;

   92 

   93         result = NativeMethods.LookupAccountName(_ComputerName, _Account.Value,

   94             binarySid, ref binarySidLength, referencedDomain,

   95             ref referencedDomainLength, out _AccountType);

   96 

   97         if (!result) throwMarshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());

   98 

   99         _Sid = newSecurityIdentifier(binarySid, 0);

  100 

  101     }

  102 

  103     privatevoid TranslateFromSecurityDescriptor()

  104     {

  105         byte[] binarySid;

  106         StringBuilder name;

  107         StringBuilder domain;

  108         uint nameLength;

  109         uint domainLength;

  110         bool result;

  111 

  112         name = newStringBuilder(0x100);

  113         domain = newStringBuilder(0xff);

  114 

  115         nameLength = (uint) name.Capacity - 2;

  116         domainLength = (uint) domain.Capacity - 2;

  117 

  118         binarySid = newbyte[_Sid.BinaryLength];

  119         _Sid.GetBinaryForm(binarySid, 0);

  120 

  121         result = NativeMethods.LookupAccountSid(

  122             _ComputerName, binarySid, name, ref nameLength, domain, ref domainLength, out _AccountType);

  123 

  124         if (!result) throwMarshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());

  125 

  126         _Account = newNTAccount(domain.ToString(), name.ToString());

  127     }

  128 

  129     #region IEquatable<NTAccount> Members

  130 

  131     ///<summary>

  132     /// Compares this identity with the specified othe identity.

  133     ///</summary>

  134     ///<param name="other">The identity to compare this identity with.</param>

  135     ///<returns>True, if both identities represent the same identity</returns>

  136     publicbool Equals(IdentityReference other)

  137     {

  138         if (other == null) returnfalse;

  139 

  140         if (other isNTAccount) return Account.Equals(other);

  141         elseif (other isSecurityIdentifier) Sid.Equals((SecurityIdentifier) other);

  142 

  143         returnfalse;

  144     }

  145 

  146     #endregion

  147 }

The interesting stuff happens in the TranslateFromSecurityDescriptor and the TranslateFromNTAccount method. They prepare some buffers and call the LookupAccountSid and LookupAccountName function. On return of either these functions, the account type is stored in the _AccountType field.

The various account types

The example code above exposes the account type as an enumeration. Here is the implementation of this enumeration:

    1 ///<summary>

    2 /// Defines the various account types of a Windows accunt

    3 ///</summary>

    4 publicenumAccountType

    5 {

    6     ///<summary>

    7     /// No account type

    8     ///</summary>

    9     None = 0,

   10     ///<summary>

   11     /// The account is a user

   12     ///</summary>

   13     User,

   14     ///<summary>

   15     /// The account is a security group

   16     ///</summary>

   17     Group,

   18     ///<summary>

   19     /// The account defines a domain

   20     ///</summary>

   21     Domain,

   22     ///<summary>

   23     /// The account is an alias

   24     ///</summary>

   25     Alias,

   26     ///<summary>

   27     /// The account is a well-known group, such as BUILTIN\Administrators

   28     ///</summary>

   29     WellknownGroup,

   30     ///<summary>

   31     /// The account was deleted

   32     ///</summary>

   33     DeletedAccount,

   34     ///<summary>

   35     /// The account is invalid

   36     ///</summary>

   37     Invalid,

   38     ///<summary>

   39     /// The type of the account is unknown

   40     ///</summary>

   41     Unknown,

   42     ///<summary>

   43     /// The account is a computer account

   44     ///</summary>

   45     Computer

   46 }

Interop methods

To use the Win32 functions from .NET, we must implement a class with the interop definitions for the two methods:

    1 staticclassNativeMethods

    2 {

    3     [SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]

    4     [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]

    5     [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]

    6     [return: MarshalAs(UnmanagedType.Bool)]

    7     publicstaticexternbool LookupAccountSid(

    8         [In] string systemName,

    9         [In, MarshalAs(UnmanagedType.LPArray)] byte[] sid,

   10         [Out] StringBuilder name,

   11         [In, Out] refuint nameLength,

   12         [Out] StringBuilder referencedDomainName,

   13         [In, Out] refuint referencedDomainNameLength,

   14         [Out] outAccountType usage);

   15 

   16     [SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]

   17     [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]

   18     [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]

   19     [return: MarshalAs(UnmanagedType.Bool)]

   20     publicstaticexternbool LookupAccountName(

   21         [In] string systemName,

   22         [In] string accountName,

   23         [Out, MarshalAs(UnmanagedType.LPArray)] byte[] sid,

   24         [In, Out] refuint sidSize,

   25         [Out] StringBuilder referencedDomainName,

   26         [In, Out] refuint referencedDomainNameLength,

   27         [Out] outAccountType accountType);

   28 }

This class allows us to call the methods from .NET


Technorati:

Posted by Henning Krause on Monday, March 12, 2007 12:00 AM, last modified on Monday, March 12, 2007 8:00 PM
Permalink | Post RSSRSS comment feed

Decoding entry ids

The EntryId (stored in the property http://schemas.microsoft.com/mapi/proptag/xfff0102) is a binary structure uniquely identifying elements in the Exchange store. It basically consists of two identifiers (three for folders): The MAPI store where the element is located (either a mailbox or a public folder tree), the folder identifier and the message identifier. Unfortunately, the store identifier cannot be mapped to a mailbox or public folder tree, so you must know in which store the element is located. But this should not be an issue in most cases. The folder and item identifiers consists of a global unique identifier and a sequential number. The combined value of these identifiers can be used to construct a permanent url (found in the property http://schemas.microsoft.com/exchange/permanenturl).

A permanent url looks like this: http://server/exchange/user/-FlatUrlSpace-/8996592620a48a4393901c9368f7c518-1488/8996592620a48a4393901c9368f7c518-1af3

This url never changes unless the item is moved to another folder.

Below is method which constructs such a permanent url based on the url to the store (the http://server/exchange/user in the above example) and an EntryId structure:

    1 using System;

    2 using System.Globalization;

    3 using System.IO;

    4 

    5 namespace InfiniTec.Exchange

    6 {

    7     staticclassExchangeStoreReference

    8     {

    9         publicstaticstring DecodeEntryId(BinaryReader reader, string baseUrl)

   10         {

   11             Guid folderId;

   12             ulong folderCnt;

   13 

   14             // First reserved field

   15             reader.ReadUInt32();

   16 

   17             // Now comes the store guid.

   18             reader.ReadBytes(16);

   19 

   20             // Next reserved field

   21             reader.ReadUInt16();

   22 

   23             folderId = ReadGuid(reader);

   24             folderCnt = SwapUInt64(reader.ReadUInt64());

   25 

   26             if (!baseUrl.EndsWith("/"))

   27             {

   28                 baseUrl += "/";

   29             }

   30 

   31             if (reader.BaseStream.Length - reader.BaseStream.Position >= 24)

   32             {

   33                 Guid messageId;

   34                 messageId = ReadGuid(reader);

   35                 ulong messageCnt;

   36                 messageCnt = SwapUInt64(reader.ReadUInt64());

   37                 baseUrl += string.Format(CultureInfo.CurrentCulture, baseUrl + "/-FlatUrlSpace-/{0:N}-{1:x}/{2:N}-{3:x}", folderId, folderCnt, messageId, messageCnt);

   38             }

   39             else

   40             {

   41                 baseUrl += string.Format(CultureInfo.CurrentCulture, baseUrl + "/-FlatUrlSpace-/{0:N}-{1:x}", folderId, folderCnt);

   42             }

   43             return baseUrl;

   44 

   45         }

   46 

   47         privatestaticGuid ReadGuid(BinaryReader reader)

   48         {

   49             int a;

   50             short b, c;

   51 

   52             a = SwapInt(reader.ReadUInt32());

   53             b = reader.ReadInt16();

   54             c = SwapShort(reader.ReadUInt16());

   55             returnnewGuid(a, b, c, reader.ReadBytes(8));

   56         }

   57 

   58         privatestaticshort SwapShort(ushort value)

   59         {

   60             unchecked

   61             {

   62                 ushort result;

   63                 result = (ushort)​(((value & 0xFF00) >> 8) |

   64                                ((value & 0x00FF) << 8));

   65 

   66                 return (short)result;

   67             }

   68         }

   69 

   70         privatestaticint SwapInt(uint value)

   71         {

   72             uint result;

   73 

   74             result = ((value & 0xFF000000) >> 24) |

   75                     ((value & 0x00FF0000) >> 8) |

   76                     ((value & 0x0000FF00) << 8) |

   77                     ((value & 0x000000FF) << 24);

   78 

   79             unchecked

   80             {

   81                 return (int)result;

   82             }

   83         }

   84 

   85 

   86         privatestaticulong SwapUInt64(ulong value)

   87         {

   88             uint lo;

   89             uint hi;

   90             ulong result;

   91 

   92             lo = (uint)​(value & 0xffffffff);

   93             hi = ((uint)​(value >> 32)) & 0xffffffff;

   94 

   95             lo = ((lo & 0xFF000000) >> 8) |

   96                 ((lo & 0x00FF0000) << 8) |

   97                 ((lo & 0x0000FF00) >> 8) |

   98                 ((lo & 0x000000FF) << 8);

   99 

  100             hi = ((hi & 0xFF000000) >> 8) |

  101                 ((hi & 0x00FF0000) << 8) |

  102                 ((hi & 0x0000FF00) >> 8) |

  103                 ((hi & 0x000000FF) << 8);

  104 

  105             result = (((ulong)lo) << 32) | hi;

  106 

  107             return result;

  108         }

  109     }

  110 }

As you can see, there is some bit-flipping going on during the process. This is because the guids in the EntryId are stored differently than they are used in the permanent url. At last, the folder id, folder sequence number, element id and element sequence number are concatenated and appended to the base url. The result is a permanent url which can be used to access the element.

To use this method, just wrap the binary value of the EntryId in a BinaryReader class and pass that instance to the DecodeEntryId method.

Remarks

  • The method above will not work for all EntryIds. Depending on how the EntryId was obtained, it may have a different structure. This is because EntryIds can either be short-term or long-term identifiers and are usually just passed to the IMsgStore::OpenEntry method within a MAPI session. But an EntryId obtained through the http://schemas.microsoft.com/mapi/proptag/xfff0102 property should work.
  • If you obtained the EntryId via a WebDAV PROPFIND command, you must decode the BASE64 string to a binary array using the Convert.FromBase64String() method.

Technorati:

Posted by Henning Krause on Saturday, March 10, 2007 12:00 AM, last modified on Thursday, March 8, 2007 9:00 PM
Permalink | Post RSSRSS comment feed