InfiniTec - Henning Krauses Blog

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

Change the MAC address of a network adapter

Affected products

  • Microsoft Windows 2000
  • Microsoft Windows 2003
  • Microsoft Windows XP

Summary

This articles explains how to change the MAC address of a network adapter.

More information

The MAC address uniquely identifies the network card within the current network segment. It consists of a vendor id that is unique among all network vendors, and a relative id that is unique to the vendor.
The address is hard-coded onto the network adapter. But since most of the drivers were deleveloped with the Windows Driver Development kit, the MAC adress is read from the Windows registry, when the card is initialized.

Steps

  1. Note the description and current MAC address of the card you want to modify. To do this, open a command prompt and type the following:
    ipconfig /all
  2. The description of the card is in the field "Description", whereas the MAC address can be found at the field "Physical address".
  3. Start the registry editor (regedt32 under Windows 2000, regedit under Windows XP and Windows 2003) and open the node
    HKEY_LOCAL_MASHINE\System\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002BE10318}.
    When you expand this node, you will find several subnodes, each representing one of your network adapters.
  4. Find the node which contains the field "Description" in the right pane that matches the description of the network adapter you want to change.
  5. On the node with the network adapter to change, check whether there is an entry named "NetworkAddress" (without the quotation marks, of course). If there is such an entry, check if the entry type is "REG_SZ". If not, delete the entry and recreate it with the appropriate type. If the entry does not yet exist, create one.
  6. Double click the entry "NetworkAddress" and enter the new network address. Note that the new network address should have exactly twelve digits.
  7. Once you entered the new network address, the network adapter has to be restarted. To do this, open the control panel and then "Network and Dialup connections". Right click the network connection you have just modified, and then click disable. After the device has been disabled, right-click the connection again and click Enable.
  8. Open a command prompt, and type
    ipconfig /all
    to check, whether the new MAC address has been changed.
    If the displayed physical address is still the same, your network card is probably incapable of changing this setting.

Posted by Henning Krause on Friday, December 31, 2004 1:14 PM, last modified on Monday, November 22, 2010 10:43 AM
Permalink | Post RSSRSS comment feed

HttpWebRequest fails when several connections are made concurrently

Affected products

  • .NET Framework 1.0
  • .NET Framework 1.1

Summary

When making several connections concurrently with the HttpWebRequest class, the request objects fail with an exception.

Symptoms

When making several connections concurrently (more than 12 on a single processor machine) with the HttpWebRequest class, the request objects fail with an exception.
If these calls are made from methods running on ThreadPool thhreads, the maximum number of connections is further reduced.

Cause

The HttpWebRequest uses two threads for each connection from the ThreadPool. This pool has a maximal capacity of 25 on one processor machines. Regularly, if the pool is full, it would queue further requests. But the HttpWebRequest class checks whether two threads are available before putting its requests into the queue. It fails with an exception, when there are less than two threads available.

Status

This behaviour is by design.

Solution

Keep concurrent connections as low as possible, and don't initiate requests from methods running on ThreadPool threads.

Other information

To keep the concurrent connections low, you can use the Advanced Threadpool from the Toolbox area of this page. This class implements a thread pool, which is independent of the ThreadPool class. With this class, you can set the maximium number of concurrent threads, so that the limit of the ThreadPool is never reached.

Posted by Henning Krause on Friday, December 31, 2004 1:09 PM, last modified on Friday, December 31, 2004 1:10 PM
Permalink | Post RSSRSS comment feed

Mount a CD/DVD image via context menu in the Explorer using Daemon-Tools

More Information

The daemon tools are by default set up to start a small program that is accessed via the system tray, near the clock. With a right-click on the program icon, one can easily mount and unmount images, or change the number of virtual drives on the fly.

However, one often comes accross an image with an explorer window. To mount such an icon, the user must do the following:

  1. Right-click on the daemon-tools tray icon

  2. Navigate to the virtual drive, where the image should be mounted

  3. Choose Mount image.

  4. In the Open file dialog, navigate to the folder

  5. Click Ok.

Luckily, the daemon tools have a rich set of command line parameters, so most of the above steps can be eliminated.

     

Solution

To allow the mounting of images via the context menu, the image files must be associated with the daemon tools. To do this apply the following registry information to your computer:

    1 Windows Registry Editor Version 5.00

    2 [HKEY_CLASSES_ROOT\DaemonTools.File]

    3 @="CD/DVD Image"

    4 [HKEY_CLASSES_ROOT\DaemonTools.File\shell]

    5 @="open"

    6 [HKEY_CLASSES_ROOT\DaemonTools.File\shell\mount1]

    7 @="Mount Image on Second Virtual Drive"

    8 [HKEY_CLASSES_ROOT\DaemonTools.File\shell\mount1\command]

    9 @="\"C:\\Program Files\\D-Tools\\daemon.exe\" -mount 1,\"%l\""

   10 [HKEY_CLASSES_ROOT\DaemonTools.File\shell\open]

   11 @="Mount Image on First Virtual Drive"

   12 [HKEY_CLASSES_ROOT\DaemonTools.File\shell\open\command]

   13 @="\"C:\\Program Files\\D-Tools\\daemon.exe\" -mount 0,\"%l\""

   14 [HKEY_CLASSES_ROOT\.iso]

   15 @="DaemonTools.File"

   16 [HKEY_CLASSES_ROOT\.nrg]

   17 @="DaemonTools.File"

   18 [HKEY_CLASSES_ROOT\.bin]

   19 @="DaemonTools.File"

   20 [HKEY_CLASSES_ROOT\.cue]

   21 @="DaemonTools.File"

   22 [HKEY_CLASSES_ROOT\.bwt]

   23 @="DaemonTools.File"

   24 [HKEY_CLASSES_ROOT\.cdi]

   25 @="DaemonTools.File"

   26 [HKEY_CLASSES_ROOT\.ccd]

   27 @="DaemonTools.File"

   28 [HKEY_CLASSES_ROOT\.mds]

   29 @="DaemonTools.File"

   30 [HKEY_CLASSES_ROOT\.pdi]

   31 @="DaemonTools.File"

The above registry file is designed for two virtual drives. If you have more or less drives, you'll have to modify this area of the file:

    1 [HKEY_CLASSES_ROOT\DaemonTools.File\shell\mount1]

    2 @="Mount Image on Second Virtual Drive"

    3 [HKEY_CLASSES_ROOT\DaemonTools.File\shell\mount1\command]

    4 @="\"C:\\Program Files\\D-Tools\\daemon.exe\" -mount 1,\"%l\""

    5 [HKEY_CLASSES_ROOT\DaemonTools.File\shell\open]

    6 @="Mount Image on First Virtual Drive"

    7 [HKEY_CLASSES_ROOT\DaemonTools.File\shell\open\command]

    8 @="\"C:\\Program Files\\D-Tools\\daemon.exe\" -mount 0,\"%l\""

    9 For a third drive, simply add the following lines:

   10 [HKEY_CLASSES_ROOT\DaemonTools.File\shell\mount2]

   11 @="Mount Image on Third Virtual Drive"

   12 [HKEY_CLASSES_ROOT\DaemonTools.File\shell\mount2\command]

   13 @="\"C:\\Program Files\\D-Tools\\daemon.exe\" -mount 2,\"%l\""

 

Note that you must replace the path to the Daemon tools folder.

Downloads

daemontools_2drives_english.zip (548 Bytes)
A registry file preconfigured for 2 drives, for an english version of Windows.
daemontools_2drives_german.zip (541 Bytes)
A registry file preconfigured for 2 drives, for a german version of Windows.

Technorati:

Posted by Henning Krause on Friday, December 31, 2004 12:00 AM, last modified on Sunday, November 28, 2010 5:12 PM
Permalink | Post RSSRSS comment feed

Retrieve all MAPI Properties of an Exchange Item

Solution

The Web Storage System of the Exchange system allows one to access the various properties of any item (e.g. a mail or appointment) via WebDAV.

To retrieve a list of all available properties, the WebDAV Propfind method is used, as displayed below:

    1 PROPFIND /public/container/ HTTP/1.1

    2 Host: www.contoso.com

    3 Depth: 0

    4 Content-type: text/xml;

    5 Content-Length: XXXXX

    6 

    7 <?xmlversion="1.0" ?>

    8 <D:propfindxmlns:D="DAV:">

    9   <D:allprop />

   10 </D:propfind>

However, this request will only return the named properties, such as the displayname, etc. The MAPI-Properties are not included in the list. Instead, the following request should be used, which uses the undocumented http://schemas.microsoft.com/exchange/allprop tag:

    1 PROPFIND /public/container/ HTTP/1.1

    2 Host: www.contoso.com

    3 Depth: 0

    4 Content-type: text/xml;

    5 Content-Length: XXXXX

    6 

    7 <?xmlversion="1.0" ?>

    8 <D:propfindxmlns:D="DAV:"xmlns:E="http://schemas.microsoft.com/exchange/">

    9   <D:allprop>

   10     <E:allprop />

   11   </D:allprop>

   12 </D:propfind>


Technorati:

Posted by Henning Krause on Friday, December 31, 2004 12:00 AM, last modified on Tuesday, April 11, 2006 12:00 PM
Permalink | Post RSSRSS comment feed

Access the Exchange store via WebDAV with Form-Based-Authentication turned on [Updated]

When a HTTP or WebDAV request is issued agains an Exchange 2003 or 2007 public folder or mailbox folder, the server returns a HTTP/1.1 440 Login Timeout error if Form Based authentication is enabled.

To work around this situation, you must do a logon to Outlook Web Access before performing any further requests. To do this, perform the following request against the exchange server:

    1 POST /exchweb/bin/auth/owaauth.dll HTTP/1.0

    2 Content-Type: application/x-www-form-urlencoded

    3 Connection: Keep-Alive

    4 User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.1.4322)

    5 Host: <Server>

    6 Content-Length: XXX

    7 

    8 destination=<destination-url>&username=<domain\\password>&password=<password>

Note: The url above is used with Exchange 2003. If want to authenticate against Exchange 2007 use this url:

    1 POST owa/auth/owaauth.dll

The Exchange 2003 url is still there but it won't work.

Replace the<destination-url> with the url of the uri where you want to do your request. The<domain\username> and the <password> fields must be replaced with the credential you want to use during your session.

When the login was successful, you will receive two cookies that you must pass along with all further requests. If no cookies are returned, the logon was not successful.

C# Example

Below is a sample code written in C#:

    1 privateHttpWebRequest GetRequestObject(string uri, string method)

    2 {

    3     HttpWebRequest webrequest;

    4     string[] credentials;

    5     CredentialCache cache;

    6     Uri _uri;

    7 

    8     // Initiate a new WebRequest to the given URI.

    9     _uri = newUri(uri);

   10     webrequest = (HttpWebRequest)System.Net.WebRequest.Create(_uri);

   11 

   12     webrequest.CookieContainer = newCookieContainer();

   13     webrequest.Method = method;

   14 

   15     return webrequest;

   16 }

   17 

   18 privateCookieCollection DoFormbasedAuthentication(string uri, NetworkCredential credential)

   19 {

   20     string server;

   21     HttpWebRequest request;

   22     HttpWebResponse response;

   23     byte[] body;

   24     Stream stream;

   25     string result;

   26 

   27     try

   28     {

   29         // Get the server portion of the requested uri and appen the authentication dll from Exchange

   30         server = uri.Substring(0, uri.IndexOf("/", 8)) + "/exchweb/bin/auth/owaauth.dll";

   31 

   32         request = GetRequestObject(server, "POST");

   33         request.CookieContainer = newCookieContainer();

   34         request.ContentType = "application/x-www-form-urlencoded";

   35 

   36         // Prepare the body of the request

   37         body = Encoding.UTF8.GetBytes(string.Format("destination={0}&username={1}\\{2}&password={3}",

   38             uri, credential.Domain, credential.UserName, credential.Password));

   39 

   40         request.ContentLength = body.Length;

   41 

   42         // Send the request to the server

   43         stream = request.GetRequestStream();

   44         stream.Write(body, 0, body.Length);

   45         stream.Close();

   46 

   47         // Get the response

   48         response = (HttpWebResponse)request.GetResponse();

   49 

   50         // Check if the login was successful

   51         if (response.Cookies.Count < 2) throw

   52             newAuthenticationException("Failed to login to OWA. Be sure your domain and password are correct.");

   53 

   54         return response.Cookies;

   55     }

   56     catch (AuthenticationException ex)

   57     {

   58         throw;

   59     }

   60     catch (Exception ex)

   61     {

   62         thrownewException("Failed to login to OWA. The following error occured: " + ex.Message, ex);

   63     }

   64 }

JavaScript Example

The same approach is possible with a small javascript.

Be sure to read this arcticle about the XmlHttp object: Permission Denied Error with XMLHTTP object

First, the main function for the example:

    1 var strCookies;

    2 var strRequest;

    3 var objXmlResult;

    4 

    5 try

    6 {

    7     strCookies = doFBALogin("https://exchangeserver/public", "exchangeserver", "domain\\username ", "password");

    8 

    9     strRequest = "<D:searchrequest xmlns:D = \"DAV:\" ><D:sql>SELECT \"DAV:displayname\" FROM \"https://exchangeserver/public\" WHERE \"DAV:ishidden\" = false</D:sql></D:searchrequest>";

   10 

   11     objXmlResult = Search("https://exchangeserver/public", strRequest, strCookies);

   12 

   13     // Do something with the XML object

   14 }

   15 catch (e)

   16 {

   17     // Handle the error!

   18 }

Now, do the actual work:

    1 function Search(strUrl, strQuery, strCookies)

    2 {

    3     // Send the request to the server

    4     var xmlHTTP = new ActiveXObject("Microsoft.xmlhttp");

    5     var i;

    6 

    7     xmlHTTP.open("SEARCH", strUrl, false);

    8     xmlHTTP.setRequestHeader("Content-type:", "text/xml");

    9 

   10     // Add the cookies to the request. According to Microsoft article Q234486, the cookie has to be set two times.

   11     xmlHTTP.setRequestHeader("Cookies", "Necessary according to Q234486");

   12     xmlHTTP.setRequestHeader("Cookies", strCookies);

   13     xmlHTTP.send(strQuery);

   14 

   15     return xmlHTTP.responseXML;

   16 }

   17 

   18 

   19 function doFBALogin(strDestination, strServer, strUsername, strPassword)

   20 {

   21     var xmlHTTP = new ActiveXObject("Microsoft.xmlhttp");

   22     var strUrl

   23         var strContent;

   24     var arrHeader;

   25     var strCookies;

   26     var intCookies;

   27     var i;

   28 

   29     // Prepare the URL for FBA login

   30     strUrl = "https://" + strServer + "/exchweb/bin/auth/owaauth.dll";

   31 

   32     xmlHTTP.open("POST", strUrl, false);

   33 

   34     xmlHTTP.setRequestHeader("Content-type:", "application/x-www-form-urlencoded");

   35 

   36     // Generate the body for FBA login

   37     strContent = "destination=" + strDestination + "&username=" + strUsername + "&password=" + strPassword;

   38     xmlHTTP.send(strContent);

   39 

   40     // Get all response headers

   41     arrHeader = xmlHTTP.getAllResponseHeaders().split("

   42         strCookies = "";

   43 

   44     intCookies = 0;

   45 

   46     // Iterate through the collection of returned headers

   47     for (i = 0; i<arrHeader.length; i++)

   48     {

   49         // If the entry is a cookies, extract the name/value

   50         if (arrHeader[i].indexOf("Set-Cookie") != -1)

   51         {

   52             strCookies += arrHeader[i].substr(12) + "; ";

   53             intCookies++;

   54         }

   55     }

   56 

   57     // Check if two cookies have been returned. Otherwise throw an exception

   58     if (intCookies < 2) throw"Could not log in to OWA!";

   59     return strCookies;

   60 }


Technorati:

Posted by Henning Krause on Friday, December 31, 2004 12:00 AM, last modified on Saturday, November 27, 2010 5:06 PM
Permalink | Post RSSRSS comment feed

Find an user in a multi-domain Active Directory enironment programmatically

More Information

An often given solution to the problem outlined above is something like this (C#):

    1 public void FindUser(string name)

    2 {

    3     DirectoryEntry gc;

    4     DirectoryEntry searchRoot;

    5     DirectorySearcher searcher;

    6     SearchResultCollection result;

    7 

    8     // Get the directoryentry of the Global Catalog root

    9     gc = new DirectoryEntry("GC:", username, password, AuthenticationTypes.Secure);

   10 

   11     // This node has exactly one child, which can be used to search the entire forest

   12     foreach (DirectoryEntry child in gc.Children)

   13     {

   14         searchRoot = child;

   15     }

   16 

   17     // Search the forest for the user object

   18     searcher = new DirectorySearcher(searchRoot,

   19     string.Format("(&(objectCategory=person)​(objectClass=user)​(sAMAccountName={0}))", name),

   20     new string[] { "distinguishedName" }, SearchScope.Subtree);

   21 

   22     result = searcher.FindAll;

   23 }

Now, the result variable contains all user-accounts with the given account name. But in a multi-domain environment, several users may have the same account name.
So, one must iterate through the list to determine the domain of each object. Unfortunately, that information is not stored within the user object.
Another drawback is, that the above solution can only be used from a computer which is logged on to a domain within the forest. From outside the forest, this will not work.
The solution in the next paragraph shows how to work around these issues.

Solution

To find an user-account object based on the domain\username information, follow the following steps:

  1. Get the directory entry for RootDse (LDAP://RootDse or GC://RootDse).
  2. From that entry, retrieve distinguished name for the configuration naming context (Property configurationNamingContext).
  3. Get the directory entry CN=<domain name>, CN=Partitions, <distinguished name of configuration naming context>. Replace the <domain name> with the domain part of the username, and the <distinguished name of configuration naming context> with the distinguished name of the configuration naming context found above. If no such directory entry exists, the given domain was invalid.
  4. From that entry, get the naming context of the domain (Property nCName).
  5. Now that you have the distinguished name of the domain where the desired user account is located, you can do a search on that domain (depending on your environment and the required information from the user object, it may or may not be a good idea to use the global catalog).
    To find the user object in domain specified above, do a search with this filter: (&(objectCategory=person)(objectClass=user)(sAMAccountName=<username>)) (Replace the <username> with the name of the account you are searching for).
    This search will either return one found directory entry, or nothing if no user with the given account name exists.

C# Example

Below is a sample in C#:

Note: This code assumes you have a DirectoryEntry pointing to the Configuration-Naming Context

    1 /// <summary>

    2 /// Searches the forrest for the directory entry of the given user.

    3 /// </summary>

    4 /// <param name="principalname">Name of the user to find. Must be in the form domain</param>

    5 /// <param name="useGC">Specifies whether to use the Global Catalog to find the user. If false, a standard LDAP-Query is used.</param>

    6 /// <returns>The directoryentry of the user, if it is found. Null otherwise.</returns>

    7 public DirectoryEntry FindUser(string principalname, bool useGC)

    8 {

    9     DirectoryEntry searchRoot;

   10     DirectorySearcher searcher;

   11     string[] name;

   12     string ncName;

   13 

   14     name = principalname.Split('\\\\');

   15     if (name.Length != 2) throw new ArgumentException("principalname is not in the correct format", principalname);

   16 

   17     ncName = ResolveNetBiosNameToDN(name[0]);

   18 

   19     searchRoot = GetDirectoryEntry(ncName, useGC);

   20 

   21     searcher = new DirectorySearcher(searchRoot,

   22                                     string.Format("(&(objectCategory=person)​(objectClass=user)​(sAMAccountName={0}))", name[1]),

   23                                     new string[] {"distinguishedName"}, SearchScope.Subtree);

   24 

   25     try

   26     {

   27         return searcher.FindOne().GetDirectoryEntry();

   28     }

   29     catch (NullReferenceException ex)

   30     {

   31         throw new ArgumentException("The given username was not found", "principalname", ex);

   32     }

   33 }

   34 

   35 private string ResolveNetBiosNameToDN(string netbiosName)

   36 {

   37     try

   38     {

   39         return (string) GetDirectoryEntry(string.Format("CN={0}, CN=Partitions, {1}", netbiosName, (string) ConfigurationNamingContext.Properties["distinguishedName"].Value)).Properties["nCName"].Value;

   40     }

   41     catch (System.Runtime.InteropServices.COMException ex)

   42     {

   43         if ((uint) ex.ErrorCode == 0x80072030) throw new ArgumentException("The given netbios name was invalid", "netbiosName", ex);

   44         else throw;

   45     }

   46 }

   47 

   48 public DirectoryEntry GetDirectoryEntry(string distinguishedName)

   49 {

   50     return GetDirectoryEntry(distinguishedName, false);

   51 }

   52 

   53 public DirectoryEntry GetDirectoryEntry(string distinguishedName, bool useGC)

   54 {

   55     string path;

   56 

   57     path = (useGC) ? "GC://" : "LDAP://";

   58     if (_Server != null) path += _Server;

   59 

   60     if ((!path.EndsWith("/")) && (distinguishedName != "")) path += "/";

   61     path += distinguishedName;

   62 

   63     if (path.EndsWith("//")) path = path.Remove(path.Length-2, 2);

   64 

   65     return new DirectoryEntry(path, Username, Password, _AuthenticationType);

   66 }


Posted by Henning Krause on Friday, December 31, 2004 12:00 AM, last modified on Monday, November 29, 2010 9:25 PM
Permalink | Post RSSRSS comment feed

Retrieving a users availability (free/busy data)

This article was formerly published under the title "Get Free/Busy data for an Exchange 2000/2003 user via HTTP requests".

For each user, Exchange stores a free/busy data on the server. Exchange 2000 and 2003 store the free/busy information in a public folder. Exchange 2007 can also do this for compatibility, but it offers a far more convenient way to get the availability, namely a WebService.

With Exchange 2000/2003 we are left with two options to read the availability data: Use OWA and pretend to be an Internet Explorer 6 or later, or access the free/busy data directly. I will cover each option in this arcticle.

Accessing the availability data directly

The availability information is stored in the NON_IPM_SUBTREE folder hierarchy below the Public Folders hierarchy in a folder called EX:/o=<Name of your organization>. For each administrative group, there is a subfolder. Each one of those folders contains the availability data for the users associated with that administrative group.

The question is: How do I get the right item in those folder for a particular user?

The answer comes in the form of the legacyExchangeDistinguishedName property from the Active Directory object of the user.


ADSIEdit showing the legacyExchangeDistinguishedName of a user (click to enlarge)

Once you have the legacyExchangeDistinguishedName of a user, you'll have to split it up. From position 0 to the slash ("/") before cn=Recipients and from that index to the end of the string. To construct the item url, you can use this method:

    1 publicstaticstring GetFreeBusyItemUrl(string legacyExchangeDistinguishedName)

    2         {

    3             string url;

    4             string orgFolder;

    5             string ouFolder;

    6 

    7             url = "http://myserver/public/NON_IPM_SUBTREE/SCHEDULE%2B%20FREE%20BUSY/EX:";

    8 

    9             orgFolder = legacyExchangeDistinguishedName;

   10             orgFolder = orgFolder.Substring(0, orgFolder.IndexOf("/cn=Recipients"));

   11             ouFolder = legacyExchangeDistinguishedName.Substring(orgFolder.Length);

   12 

   13             url += HttpUtility.UrlPathEncode(orgFolder.Replace("/", "_xF8FF_")) + "/USER-" +

   14                    HttpUtility.UrlPathEncode(ouFolder.Replace("/", "_xF8FF_")) + ".eml";

   15 

   16             return url;

   17         }

Since the legacyExchangeDistinguishedName contains a slash in the item name, you must encode it with is unicode representation, _xF8FF_, otherwise Exchange would treat it as a folder delimiter.

Once you have the url to the free/busy item, you can issue a PROPFIND on the item and retrieve these properties:

  • x68531003 (Busy months)
  • x68541102 (Busy events)
  • x68511003 (Tentative months)
  • x68521102 (Tentative events)
  • x68551003 (Out of office months)
  • x68561102 (Out of office events)
  • x68470003 (Start of the published range)
  • x68480003 (End of the published range)

All these properties live in the http://schemas.microsoft.com/mapi/proptag/ namespace.

We'll start with the last two properties. These contain the start date and the end date of the published data. These values always fall on month boundaries and are encoded in systime, a MAPI datatype.

You can use this code to convert a systime to a DateTime:

    1 ///<summary>

    2 /// Parses a MAPI systime and converts it to a <see cref="DateTime"/> instance.

    3 ///</summary>

    4 ///<param name="value">The value.</param>

    5 ///<returns></returns>

    6 privatestaticDateTime ParseSysTime(int value)

    7 {

    8     DateTime result;

    9 

   10     result = newDateTime(1601, 1, 1, 0, 0, 0, DateTimeKind.Utc) + TimeSpan.FromMinutes(value);

   11 

   12     return result.ToLocalTime();

   13 }

The other properties contain the actual free/busy data. For each free/busy status there are two properties: One describes the months which are covered, and one contains the availability data for those months. For example, the appointments marked as busy are recorded in the properties x68531003 and x68541102. The first property has a datatype of mv.int, the latter one is a mv.binary. Both arrays should have the same length, otherwise, the free/busy data is corrupted. To decode the values, you'll have to iterate through those arrays and decode the information. You can use this code snippet to do this:

    1 privatevoid ParseFreeBusyEntries(IList<int> monthValues, IList<byte[]> eventEntries, FreeBusyStatus freeBusyStatus)

    2 {

    3     BinaryReader reader;

    4     DateTime monthStart;

    5     DateTime start;

    6     DateTime end;

    7 

    8     for (int i = 0; i < Math.Min(monthValues.Count, eventEntries.Count); i++)

    9     {

   10         monthStart = newDateTime(monthValues[i] >> 4, monthValues[i] & 15, 1);

   11 

   12         reader = newBinaryReader(newMemoryStream(eventEntries[i]));

   13 

   14         while (reader.BaseStream.Length - reader.BaseStream.Position >= 4)

   15         {

   16             start = (monthStart + TimeSpan.FromMinutes(reader.ReadUInt16())).ToLocalTime();

   17             end = (monthStart + TimeSpan.FromMinutes(reader.ReadUInt16())).ToLocalTime();

   18 

   19             StoreFreeBusyEntry(start, end, freeBusyStatus);

   20         }

   21     }

   22 }

The method requires you to extract the arrays from the xml response stream and decode the base64 encoded data to a byte array. The FreeBusyStatus type in line one is an enumeration consiting of the various free/busy status. The method iterates through the two arrays. From each entry in the monthValues array, it extracts the start date for that range. It then constructs a BinaryReader to read through the corresponding entry in the eventEntries array and extracts the event information from the stream. For each entry, the StoreFreeBusyEntry() is called to do something with the extracted information. In my library, I put them into a global list of events.

You'll have to call this method for each of the three pairs of properties. Note that not all properties are always set. If no appointment with a free/busy status of busy occurs during the published period, the two corresponding properties would be null.

This method will work with Exchange 2000 and 2003. It will also work with Exchange 2007 if you have the public folder tree installed, which is not longer mandatory.

Getting the availability by using OWA

When you create a meeting with OWA, it will display the availability information of the attendees like Outlook does. To get the information, it uses a special OWA command which returns the free/busy information as an XML stream.

Just issue this request to the server and you'll get your data:

    1 GET /public/?Cmd=freebusy&start=<startDate>&end=<endDate>&interval=<interval>&u=SMTP:<userEmail>

    2 HTTP/1.1

    3 Translate: t

    4 User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.1.4322)

    5 Host: <Server>

The <startDate> and <endDate> must be replaced with the date range you are requesting, in the format yyyy-MM-ddTHH :mm:sszzz. An example would be 2004-01-01T14:00:00+02:00 (January first, 2004, two PM with a time-zone of two hours).
<interval> specifies the interval in minutes. Minimum is 5.
Finally, the <userEmail> must be replaced with the SMTP email address of the user for which the free/busy information is requested. If you want to request the free/busy data for multiple persons for the same date range, you can add more &u=SMTP:<Emailaddress>. However, the maximum length for the GET command is 1024 bytes.
The following example would request the free/busy data for John Doe (jdoe@contoso.com) for the 25 July 2004 from two PM to four PM CET (Central European Time, GMT+01:00) with an interval of 30 minutes:

    1 GET /public/?cmd=freebusy&start=2004-07-25T14:00:00+01:00&end=2004-07-25T16:00:00+01:00&interval=30&u=SMTP:alice@contoso.com

The response will look like this:

    1 <a:responsexmlns:a="WM">

    2   <a:recipients>

    3     <a:item>

    4       <a:displayname>Alice</a:displayname>

    5       <a:emailtype="SMTP">alice@contoso.com</a:email>

    6       <a:type>1</a:type>

    7       <a:fbdata>00120</a:fbdata>

    8     </a:item>

    9   </a:recipients>

   10 </a:response>

Now, the interesting information is in the <a:fbdata> node. This node contains the requested data. Each digit represents one interval. The meaning is as follows:

  • 0 equals CdoFree
  • 1 equals CdoTentative
  • 2 equals CdoBusy
  • 3 equals CdoOutOfOffice

It is important that you send that User-Agent string along. If you don't pretend to be an Internet Explorer 6 or later, you won't get an XML response because OWA thinks you are using a down-level browser which don't understands XML.

This method will also not work with Exchange 2007, as most of the OWA commands have been removed from Exchange.


Technorati:

Posted by Henning Krause on Friday, December 31, 2004 12:00 AM, last modified on Friday, November 26, 2010 8:57 AM
Permalink | Post RSSRSS comment feed

Retrieve the size of a Microsoft Exchange 2000/2003 public folder

More Information

The size of all messages in a folder is stored on each folder object. This information can be extracted from several fields, for example the http://schemas.microsoft.com/exchange/foldersize property. But this property seems to be mapped to the MAPI property PR_MESSAGE_SIZE. The drawback with this property is, that it is limited to 32 bit, so it works only for folders containing not more than 4 GB.
Luckily, this property is also available as a 64 bit value, stored in the MAPI property PR_MESSAGE_SIZE_EXTENDED

Example

The following Visual Basic script shows how to use the above mentioned property:

    1 option explicit

    2 dim xmlhttp

    3 dim xmldoc

    4 dim strrequest

    5 dim ie

    6 

    7 strrequest = "<?xml version=""1.0"" ?><D:propfind xmlns:D=""DAV:"" xmlns:E=""http://schemas.microsoft.com/mapi/proptag/""><D:prop><E:x0e080014 /></D:prop></D:propfind>"

    8 Set xmlhttp = CreateObject("msxml2.xmlhttp")

    9 Set xmldoc = CreateObject("Msxml2.DOMDocument")

   10 xmldoc.loadXML(strrequest)

   11 

   12 wscript.echo "Connecting to " & wscript.Arguments(0) & "..."

   13 xmlhttp.Open "PROPFIND", wscript.Arguments(0), false, wscript.Arguments(1), wscript.arguments(2)

   14 xmlhttp.setRequestHeader "Content-type:", "text/xml"

   15 xmlhttp.setRequestHeader "Depth", "0"

   16 xmlhttp.send strrequest

   17 wscript.echo "Return status: " & xmlhttp.status & " " & xmlhttp.statustext

   18 

   19 set xmldoc = xmlhttp.responseXml

   20 if (xmlhttp.status = 207) then

   21     xmldoc.setProperty "SelectionNamespaces", "xmlns:ex='http://schemas.microsoft.com/mapi/proptag/'"

   22     wscript.echo "Size of folder: " & xmldoc.selectSingleNode("//ex:x0e080014").text & " Bytes"

   23 end if

To use this script, simply copy it into a text file and save it as foldersize.vbs file. Then open a command prompt and type the following:

    1 cscript foldersize.vbs http://myserver/public/myfolder username password

Where myserver is the exchange server that contains the folder you want to check and myfolder ist the name of the folder.

More Information

This script can also be run against a folder withing a mailbox of a user. To do this, open a command prompt and type the following:

    1 cscript foldersize.vbs http://myserver/exchange/alias/myfolder username password

Where alias is the alias of the user's mailbox to check.

Technorati:

Posted by Henning Krause on Friday, December 31, 2004 12:00 AM, last modified on Monday, November 29, 2010 6:42 PM
Permalink | Post RSSRSS comment feed

Get the WebDAV url for an Exchange 2000/2003 mailbox

More Information

For Exchange 2000 and Exchange 2003 prior Service Pack 1, the building of an URL suitable for WebDAV requests against a users mailbox is a rather complicated thing. A solution for this is descrbied under the section Solution.

From Exchange 2003 SP 1 onwards, you can just use his SMTP address to get access to his mailbox. For example, to access the mailbox of John Doe (jdoe@contoso.com) you would use the url

http://<myserver>/exchange/jdoe@contoso.com

Solution

To build the webaddress, you must do the following:

  1. If you only have a domain\username, not the distinguished name of the user, you must first get the latter one. See this aricle how to retrieve the name.
  2. Get the directory entry with the distinguished name of the user
  3. From that object, retrieve the property homemdb. This property contains the distinguished name of the mailbox store.
  4. Retrieve the directory entry of the mailbox store.
  5. From this object, retrieve the property named msExchOwningServer. This is the distinguished name of the Exchange server that hosts the mailbox.
  6. Get the root of the http virtual server on the exchange server. The distinguished name is CN=http, CN=Protocols, <distinguished name of the exchange server>.
  7. Search for all http virtual server (LDAP filter: (objectClass=protocolCfgHttpServer), Scope: OneLevel, retrieve these properties: msExchServerBindings, msExchDefaultDomain).
  8. From this list, retrieve the default SMTP server. It's the one without the attribute msExchDefaultDomain.
  9. Get the default SMTP domain of the organization:
    1. Open the node CN=Default Policy, CN=Recipient Policies, CN=<Name of your organization>, CN=Microsoft Exchange, CN=Services,<Distinguished name of the configuration naming context>.
    2. Retrieve the property gatewayProxy. This is a multi-valued property that contains the default addresses of the organization.
    3. Check each of the entries in that list. If it starts with SMTP:, you have found it.
  10. Get the property proxyAddresses from the directory object of the user. This is a multi-valued properties that contains all addresses that are assigned to the user. Each entry has the following consists of a protocol identifier and an address entry. For example:
    SMTP:john.doe@example.com
    If the protocol identifier is all uppercase, it is the default entry for that protocol.
  11. Iterate through the list of assigned addresses and search for the SMTP address which domain part matches with the default SMTP retrieved above.
  12. Select the correct HTTP Server:
    • If you found an email address corresponding to the default SMTP policy, extract the alias from the found address. For the above example the alias would be john.doe.  The correct HTTP server to use is the default HTTP Server
    • If there is no smtp address matching the default policy, iterate through the list of proxy addresses again, and do the following, if it is an SMTP address:
      1. Extract the alias and the domain from the address.

      2. Iterate through the list of virtual HTTP server and check the property msExchDefaultDomain of each entry. If it matches the domain of the users address domain, you have found the correct http server.

  13. Get the property msExchServerBindings from the selected HTTP server node. This entry has the following structure:
    IPAddress:Port:Servername
    The first and the last part of the string can be empty, like in this example:
    :80:
  14. Depending on the content of this field, you can now build the desired url:
    • If the servername of the msExchServerBindings property is not empty, build the OWA url like this:
      http://<Servername>:<Port>/exchange/<Alias>
    • If the servername is empty, but an ipaddress is present, build the OWA url like this:
      http://<IPAddress>:<Port>/exchange/<Alias>
    • If both, the servername and the ipaddress fields are empty, get the ipaddress for the exchange server:
      1. From the node of the exchange server, retrieve the property networkaddress.
      2. Iterate through this property and retrieve the value which starts with ncacn_ip_tcp:.
      3. Now, build the url with the above scheme, using the just found ip address.

Tags: , , , , ,

Technorati: , , , , ,

Posted by Henning Krause on Friday, December 31, 2004 12:00 AM, last modified on Tuesday, July 26, 2011 9:57 PM
Permalink | Post RSSRSS comment feed

Manage distributionlists programatically

Description

When examined via WebDAV, distribution lists are stored as items of type urn :content -classes:group. The Outlook message class (http://schemas.microsoft.com/exchange/outlookmessageclass) is IPM.DistList.
The information about the members of the distribution list is stored in the field http://schemas.microsoft.com/mapi/dlmembers. This is a multivalued field of type mv.base64. Unfortunately, this field is not documented by Microsoft.
But, as pointed out in the summary, user can manage their distribution lists via Outlook Web Access. So, one way to programatically manage distribution lists is to simulate an Outlook Web Access client. This method is used in this article.

Solution

To get the contents of a distribution list, make a GET request to the URL of the distribution list with the query-string set to Cmd=viewmembers. The User-Agent header must be set to Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.1.4322) so that the Exchange ISAPI filter knows the client can handle an xml response.
The response will look like this:

    1 <xml id="memberlist">

    2   <cdldata>

    3     <href>

    4       <!--Uri to distribution list -->

    5     </href>

    6     <error></error>

    7     <membertext></membertext>

    8     <unresolvedcount>0</unresolvedcount>

    9     <member>

   10       <memberid>0</memberid>

   11       <icon>/exchweb/img/icon-1off.gif</icon>

   12       <dn>user@example.com</dn>

   13       <email>user@example.com</email>

   14     </member>

   15     <member>

   16       <memberid>1</memberid>

   17       <icon>/exchweb/img/icon-member.gif</icon>

   18       <dn>Administrator</dn>

   19       <email>Administrator@contoso.local</email>

   20     </member>

   21   </cdldata>

   22 </xml>

The member is is espacially important when removing members from a distribution list, because it must be passed along with the request.

To add a new member to a distritubion list, make a POST request to the URL of the list and specify this body within the request:

    1 Cmd=addmember

    2 msgclass=IPM.DistList

    3 member=user@example.com

Again, the User-Agent field must be set to the value mentioned above.

On success, you will receive a HTTP/1.1 302 Moved Temporarily message that point to the url of the list with the querystring set to Cmd = viewmembers.

To remove a member from the list, make a POST request with the following body within the request:

    1 Cmd=deletemember

    2 msgclass=IPM.DistList

    3 memberid=1

Be sure to set the member id to the id of the entry you want to remove. When successful, you will receive the same response as above.


Technorati:

Posted by Henning Krause on Friday, December 31, 2004 12:00 AM, last modified on Monday, December 26, 2005 12:00 PM
Permalink | Post RSSRSS comment feed