A long time ago, I wrote an article on How to get the Global Address List programatically. The theory behind that article is still valid, but it only features a VBScript example. Since someone from the Microsoft Exchange Forum had trouble converting it to C#, I fired up Visual Studio and hacked something together. The result is somewhat more complete than the VBScript example, because it allows access to all address lists, not just the default global address list (GAL).
Here we go:
using System;
using System.Collections.Generic;
using System.DirectoryServices;
using System.Linq;
namespace AddressListSample
{
public class ActiveDirectoryConnection
{
public DirectoryEntry GetLdapDirectoryEntry(string path)
{
return GetDirectoryEntry(path, "LDAP");
}
public DirectoryEntry GetGCDirectoryEntry(string path)
{
return GetDirectoryEntry(path, "GC");
}
private DirectoryEntry GetDirectoryEntry(string path, string protocol)
{
var ldapPath = string.IsNullOrEmpty(path) ? string.Format("{0}:", protocol) : string.Format("{0}://{1}", protocol, path);
return new DirectoryEntry(ldapPath);
}
}
public class ExchangeAddressListService
{
private readonly ActiveDirectoryConnection _Connection;
public ExchangeAddressListService(ActiveDirectoryConnection connection)
{
if (connection == null) throw new ArgumentNullException("connection");
_Connection = connection;
}
public IEnumerable<AddressList> GetGlobalAddressLists()
{
return GetAddressLists("CN=All Global Address Lists");
}
public IEnumerable<AddressList> GetAllAddressLists()
{
return GetAddressLists("CN=All Address Lists");
}
public IEnumerable<AddressList> GetSystemAddressLists()
{
return GetAddressLists("CN=All System Address Lists");
}
private IEnumerable<AddressList> GetAddressLists(string containerName)
{
string exchangeRootPath;
using (var root = _Connection.GetLdapDirectoryEntry("RootDSE"))
{
exchangeRootPath = string.Format("CN=Microsoft Exchange, CN=Services, {0}", root.Properties["configurationNamingContext"].Value);
}
string companyRoot;
using (var exchangeRoot = _Connection.GetLdapDirectoryEntry(exchangeRootPath))
using (var searcher = new DirectorySearcher(exchangeRoot, "(objectclass=msExchOrganizationContainer)"))
{
companyRoot = (string) searcher.FindOne().Properties["distinguishedName"][0];
}
var globalAddressListPath = string.Format(containerName + ",CN=Address Lists Container, {0}", companyRoot);
var addressListContainer = _Connection.GetLdapDirectoryEntry(globalAddressListPath);
using (var searcher = new DirectorySearcher(addressListContainer, "(objectClass=addressBookContainer)"))
{
searcher.SearchScope = SearchScope.OneLevel;
using (var searchResultCollection = searcher.FindAll())
{
foreach (SearchResult addressBook in searchResultCollection)
{
yield return
new AddressList((string) addressBook.Properties["distinguishedName"][0], _Connection);
}
}
}
}
}
public class AddressList
{
private readonly ActiveDirectoryConnection _Connection;
private readonly string _Path;
private DirectoryEntry _DirectoryEntry;
internal AddressList(string path, ActiveDirectoryConnection connection)
{
_Path = path;
_Connection = connection;
}
private DirectoryEntry DirectoryEntry
{
get
{
if (_DirectoryEntry == null)
{
_DirectoryEntry = _Connection.GetLdapDirectoryEntry(_Path);
}
return _DirectoryEntry;
}
}
public string Name
{
get { return (string) DirectoryEntry.Properties["name"].Value; }
}
public IEnumerable<SearchResult> GetMembers(params string[] propertiesToLoad)
{
var rootDse = _Connection.GetGCDirectoryEntry(string.Empty);
var searchRoot = rootDse.Children.Cast<DirectoryEntry>().First();
using (var searcher = new DirectorySearcher(searchRoot, string.Format("(showInAddressBook={0})", _Path)))
{
if (propertiesToLoad != null)
{
searcher.PropertiesToLoad.AddRange(propertiesToLoad);
}
searcher.SearchScope = SearchScope.Subtree;
searcher.PageSize = 512;
do
{
using (var result = searcher.FindAll())
{
foreach (SearchResult searchResult in result)
{
yield return searchResult;
}
if (result.Count < 512) break;
}
} while (true);
}
}
}
internal class Program
{
private static void Main()
{
var connection = new ActiveDirectoryConnection();
var service = new ExchangeAddressListService(connection);
foreach (var addressList in service.GetGlobalAddressLists())
{
Console.Out.WriteLine("addressList.Name = {0}", addressList.Name);
foreach (var searchResult in addressList.GetMembers())
{
Console.Out.WriteLine("\t{0}", searchResult.Properties["name"][0]);
}
}
foreach (var addressList in service.GetAllAddressLists())
{
Console.Out.WriteLine("addressList.Name = {0}", addressList.Name);
foreach (var searchResult in addressList.GetMembers())
{
Console.Out.WriteLine("\t{0}", searchResult.Properties["name"][0]);
}
}
foreach (var addressList in service.GetSystemAddressLists())
{
Console.Out.WriteLine("addressList.Name = {0}", addressList.Name);
foreach (var searchResult in addressList.GetMembers())
{
Console.Out.WriteLine("\t{0}", searchResult.Properties["name"][0]);
}
}
}
}
}
The sample wraps the whole logic up into two classes: ExchangeAddressListService and AddressList. The first serves as some kind of entry point and the latter allows access to the members of a list.
Hope this helps…
de77b361-966d-4104-84b9-9f5935d5dd9e|9|3.4
Tags:
adsi, exchange 2003, exchange 2007, exchange 2010, ldap, gc, c#
Technorati:
adsi, exchange+2003, exchange+2007, exchange+2010, ldap, gc, c%23
I’ve moved to Office 365 for my private mails a while ago and. Up to now, the provider hosting this website was responsible for the DNS entries. But now I moved the entire DNS authority to Microsoft for my primary domain. This lead to the unfortunate incident that I was unable to send mails to GMX (one of the largest free mailer in Germany). Thanks to a tip from Daniel Melanchthon I was able to resolve the issue fairly quickly.
The problem seems to be that GMX checks for the existence of a DNA A record on the sender domains. My primary domain is henningkrause.eu. When I send an email to GMX, they check for an A record for henningkrause.eu. I’m not sure why they do that - I know of no RFC which makes such a record mandatory. The A record is normally only used as a fallback method in case no MX record can be found. The solution was to create such a DNS record using the Office 365 DNS management interface:
The entry in question is the one highlighted in red. The host name "@” specifies that the A record is to be created for the domain itself. The interesting thing is that it is not relevant to which IP address the A record points. It just needs to be there. If you experience the same issues and don’t have your own website, just point it to any IP address.
After I created the domain, I was able to send mails to GMX again almost immediately.
2a5b9e3c-5666-4997-878a-052247463d3e|1|5.0
A while ago, Glen posted an article on his blog on how to set homepage of a folder using ADO and later he posted a version of that script which uses the EWS managed API to do this in this MSDN forum thread. However, he wrote it for the first version of the API, and the EWS Managed API 1.1 has a slightly different object model. Since someone on the MSDN forums had difficulties to update the script to work with the EWS Managed API 1.1, I thought I just post an updated version here:
private static void SetFolderHomePage(IEnumerable<string> pathFragments, string url, ExchangeService service)
{
var folderWebviewinfoProperty = new ExtendedPropertyDefinition(14047, MapiPropertyType.Binary);
var root = Folder.Bind(service, WellKnownFolderName.MsgFolderRoot);
var targetFolder = root;
foreach (var fragment in pathFragments)
{
var result = service.FindFolders(targetFolder.Id, new SearchFilter.IsEqualTo(FolderSchema.DisplayName, fragment), new FolderView(1));
if (result.TotalCount == 0)
{
throw new InvalidOperationException(string.Format("Folder fragment {0} was not found.", fragment));
}
targetFolder = result.Folders[0];
}
targetFolder.SetExtendedProperty(folderWebviewinfoProperty, EncodeUrl(url));
targetFolder.Update();
}
private static byte[] EncodeUrl(string url)
{
var writer = new StringWriter();
var dataSize = ((ConvertToHex(url).Length / 2) + 2).ToString("X2");
writer.Write("02"); // Version
writer.Write("00000001"); // Type
writer.Write("00000001"); // Flags
writer.Write("00000000000000000000000000000000000000000000000000000000"); // unused
writer.Write("000000");
writer.Write(dataSize);
writer.Write("000000");
writer.Write(ConvertToHex(url));
writer.Write("0000");
var buffer = HexStringToByteArray(writer.ToString());
return buffer;
}
private static string ConvertToHex(string input)
{
return string.Join(string.Empty, input.Select(c => ((int) c).ToString("x2") + "00").ToArray());
}
private static byte[] HexStringToByteArray(string input)
{
return Enumerable
.Range(0, input.Length/2)
.Select(index => byte.Parse(input.Substring(index*2, 2), NumberStyles.AllowHexSpecifier)).ToArray();
}
You can set the homepage of a folder by calling the SetFolderHomepage method:
SetFolderHomePage(service, new[] {"InfiniTec blog"}, http://www.infinitec.de);
8ac429f4-e883-4308-a87f-47a478f288e8|3|4.7
I’ve seen quite a few samples for using the FindItems method of the EWS Managed API, either on StackOverflow or in the Exchange development forum. One of the most common error I see is that people disable paging by specifying creating an ItemView like this:
var view = new ItemView(int.MaxValue);
Don’t do that! A query using this ItemView will put a severe burden on the Exchange server if the folder being queried contains many items. The pageSize parameter is there for a reason.
Instead of querying all items at once, one should do a proper paging: Query, say, 512 items and process them. Then, query the next 512 items. Thanks to iterators in C#, the process of retrieving the items can be completely separated away from the processing.
If you are not familiar with C# iterators, have a look at this article. In essence, the yield operator allows for deferred execution, something you might heard from with regards to LINQ.
Here is a helper method which uses this technique to iterate over the contents of an Exchange folder, requesting 512 items per batch and returning them to the caller. When used properly, this method uses very little resources on the Exchange server as well as on the client. Additionally, it allows for querying the body of an item. In this case, instead of executing a FindItems request, it executes a FindItems request and a LoadPropertiesForItems request per batch.
Due to the nature of the method, you should not put the items returned from the methods in a List, as this would consume a lot of memory on the client. Instead process the items one by one.
public static class ExchangeServiceExtension
{
public static IEnumerable<Item> FindItems(this ExchangeService service, WellKnownFolderName folderName, PropertySet propertySet, SearchFilter searchFilter, ItemTraversal traversal = ItemTraversal.Shallow, params KeyValuePair<PropertyDefinition, SortDirection>[] orderBy)
{
return FindItems(service, new FolderId(folderName), propertySet, searchFilter, traversal, orderBy);
}
public static IEnumerable<Item> FindItems(this ExchangeService service, FolderId folderId, PropertySet propertySet, SearchFilter searchFilter = null, ItemTraversal traversal = ItemTraversal.Shallow, params KeyValuePair<PropertyDefinition, SortDirection>[] orderBy)
{
if (service == null) throw new ArgumentNullException("service");
if (folderId == null) throw new ArgumentNullException("folderId");
if (propertySet == null) throw new ArgumentNullException("propertySet");
PropertySet propertySetToQuery;
// If the body or unique body is requested, the FindItems method cannot be used to query the
// propertyset. Instead a GetItem operation is required. The propertyset IdOnly is used in this case
// for the FindItems operation.
if (propertySet.Contains(ItemSchema.Body) || propertySet.Contains(ItemSchema.UniqueBody))
{
propertySetToQuery = PropertySet.IdOnly;
}
else
{
propertySetToQuery = propertySet;
}
var itemView = new ItemView(512) { PropertySet = propertySetToQuery, Traversal = traversal };
if (orderBy != null)
{
foreach (var order in orderBy)
{
itemView.OrderBy.Add(order.Key, order.Value);
}
}
bool hasMoreItems;
do
{
var result = service.FindItems(folderId, searchFilter, itemView);
if (propertySetToQuery == PropertySet.IdOnly)
{
// Load the properties, including the body using a GetItem request.
service.LoadPropertiesForItems(result, propertySet);
}
foreach (var item in result)
{
yield return item;
}
hasMoreItems = result.MoreAvailable;
itemView.Offset += 512;
} while (hasMoreItems);
}
}
As you can see, this method uses several .NET 4.0 features: Optional parameters and extension methods. So, if you are still using .NET 2.0, you’ll need to modify the code a little bit.
To use these methods, insert this code into your solution. In the class where you want to use the methods, add a using to the namespace containing the ExchangeServiceExtension. Both methods will then appear in the code completion window on instances of the ExchangeService class:
In the following example, the method is used to iterate over all items in the Inbox folder and printing the items to the console:
var items = service.FindItems(WellKnownFolderName.Inbox, new PropertySet(BasePropertySet.IdOnly, ItemSchema.Subject));
foreach (var item in items)
{
Console.Out.WriteLine("Subject = {0}", item.Subject);
}
Since the last parameters are optional, they can be omitted. This example adds a sort clause to the call:
var items = service.FindItems(WellKnownFolderName.Inbox, new PropertySet(BasePropertySet.IdOnly, ItemSchema.Subject),
orderBy: new KeyValuePair<PropertyDefinition, SortDirection>(ItemSchema.Subject, SortDirection.Ascending));
Of course, a SearchFilter and an ItemTraversal can also be specified:
var items = service.FindItems(WellKnownFolderName.Inbox, new PropertySet(BasePropertySet.IdOnly, ItemSchema.Subject),
new SearchFilter.ContainsSubstring(ItemSchema.Subject, "test"), ItemTraversal.Associated,
new KeyValuePair<PropertyDefinition, SortDirection>(ItemSchema.Subject, SortDirection.Ascending));
3b503b7b-67d5-4ed3-8cc4-ac78257aed44|2|5.0
Every now and then people in the Exchange development forum asks whether the ItemId returned by a FindItems call is unique or not, because they allegedly got duplicate ids for one folder. The short answer to this question is: Yes, ItemIds are unique. And you are seeing duplicate ids because you are probably performing an incase-sensitive comparison.
Long answer:
In the following example, I executed a FindItems call on one of my mailbox folders and got multiple of such “duplicates”. One example:
AAMkAGY3OTBhZTFkLTAzNTgtNDM1Mi1hZjQ0LTg3YzUyOGRjM2I4NgBGAAAAAADssXJen7JoRp0FaJMBxQBjBwDTKvJ2qH1IS6bTjJryZwf0ACvTU4AKAADTKvJ2qH1IS6bTjJryZwf0ACvTWo5aAAA=
AAMkAGY3OTBhZTFkLTAzNTgtNDM1Mi1hZjQ0LTg3YzUyOGRjM2I4NgBGAAAAAADssXJen7JoRp0FaJMBxQBjBwDTKvJ2qH1IS6bTjJryZwf0ACvTU4AKAADTKvJ2qH1IS6bTjJryZwf0ACvTWo5AAAA=
These ids differ only in the 5th char from the right. The first one has a capital ‘A’ and the other has a small ‘a’ at that position. This can happen because the item id is base64 encoded. Base64 is way to represent binary data as ASCII text. A numerical value of 5 is encoded as a capital “F” and a value of 31 is also represented by an “f” - albeit a lower-case one.
If you store an item id in a database (SQL Server for example) you’ll most likely have run into the problem that a select statement with a WHERE clause on the item id returns multiple hits. This is due to the fact that the SQL Server by default uses a incase-sensitive collation for columns. To change this, open the table in question with the designer, select the column containing the item id. The designer will then display the column properties, like in this example:
Click on the Collation row and then on the button with the three full-stops. This will open the collation selection dialog:
Ensure that the checkbox labeled “Case Sensitive” is checked. Click ok and then save the table.
Note: If you do this via the SQL Server Management Studio, it will recreate the entire table (including data, indexes and constraints). Depending on the size of the table, this can take quite some time.
Here is an SQL script which changes the column collation directly:
ALTER TABLE Example
ALTER COLUMN ItemId VARCHAR(255)
COLLATE Latin1_General_CS_AS
You need to adept the script to your environment. Albeit from the name of the table, you might have chosen a different data type for the ItemId column.
If you prefer not to change the table structure you can modify the query you are using to fetch the data from the database. Your query might now look like this:
SELECT * FROM Example
WHERE ItemId = 'AAMkAGY3OTBhZTFkLTAzNTgtNDM1Mi1hZjQ0LTg3YzUyOGRjM2I4NgBGAAAAAADssXJen7JoRp0FaJMBxQBjBwDTKvJ2qH1IS6bTjJryZwf0ACvTU4AKAADTKvJ2qH1IS6bTjJryZwf0ACvTWo5aAAA='
Instead of this query, use this one:
SELECT * FROM Example
WHERE ItemId COLLATE Latin1_General_CS_AS = 'AAMkAGY3OTBhZTFkLTAzNTgtNDM1Mi1hZjQ0LTg3YzUyOGRjM2I4NgBGAAAAAADssXJen7JoRp0FaJMBxQBjBwDTKvJ2qH1IS6bTjJryZwf0ACvTU4AKAADTKvJ2qH1IS6bTjJryZwf0ACvTWo5aAAA='
This select statement will perform a case-sensitive search on the table.
719eb5c3-a009-4a69-9231-1c2ed5318afb|1|5.0
Technorati:
Todays post is a small detour from the regular Exchange related posts. The other day I was troubleshooting an issue with a program. It did not behave as expected in production and so I fired up WinDbg to inspect the value of some objects. I attached WindDbg to the process in question and load psscor4, a replacement from Microsoft for the standard SOS extension (Note that if you want to use psscor, you need the right version: Psscor2 is for .NET 2.0-3.5 software. Psscor4 is only for .NET 4 programs). The WinDbg results I present in this article are not from the process I actually examined. I hacked together a small sample program instead.
The sample class creates instances of a class named Person. A Person class has two properties: A name (string) and an age (int). The object instance I’m looking for is named Mallory. The goal is to get a look at the object instance with a call to !DumpObj. So all I need is the memory address of the Person instance named Alice. This should be easy.
So here we go. First, load the SOS extension. Since I ‘m examining an x64 process, I ‘m using the X64 version of psscor.
.load C:\temp\Psscor4\amd64\amd64\psscor4.dll
The first thing to do is to get an overview about the target class. How many instances of the Person class are currently lying around? A call to Dumpheap reveals this:
0:007> !dumpheap -type Person -stat
Loading the heap objects into our cache.
total 21 objects
Statistics:
MT Count TotalSize Change Class Name
000007ff000242b0 1 40 1 System.Collections.Generic.List`1[[DebugTest.Person, DebugTest]]
000007ff00024220 20 640 20 DebugTest.Person
So, we have one list of Person objects: List<Person> and 20 instances of the Person class itself. Since there are only 20 of them, let’s just dump them to the console:
Total 21 objects, Total size: 680
0:007> !dumpheap -type Person
Loading the heap objects into our cache.
Address MT Size
00000000027f2588 000007ff000242b0 40 0 System.Collections.Generic.List`1[[DebugTest.Person, DebugTest]]
00000000027f2588 000007ff000242b0 40
00000000027f25d0 000007ff00024220 32 0 DebugTest.Person
00000000027f25d0 000007ff00024220 32
00000000027f2630 000007ff00024220 32 0 DebugTest.Person
00000000027f2630 000007ff00024220 32
00000000027f2650 000007ff00024220 32 0 DebugTest.Person
00000000027f2650 000007ff00024220 32
00000000027f2670 000007ff00024220 32 0 DebugTest.Person
00000000027f2670 000007ff00024220 32
00000000027f2690 000007ff00024220 32 0 DebugTest.Person
00000000027f2690 000007ff00024220 32
00000000027f2710 000007ff00024220 32 0 DebugTest.Person
00000000027f2710 000007ff00024220 32
00000000027f2730 000007ff00024220 32 0 DebugTest.Person
00000000027f2730 000007ff00024220 32
00000000027f2750 000007ff00024220 32 0 DebugTest.Person
00000000027f2750 000007ff00024220 32
00000000027f2770 000007ff00024220 32 0 DebugTest.Person
00000000027f2770 000007ff00024220 32
00000000027f2830 000007ff00024220 32 0 DebugTest.Person
00000000027f2830 000007ff00024220 32
00000000027f2850 000007ff00024220 32 0 DebugTest.Person
00000000027f2850 000007ff00024220 32
00000000027f2870 000007ff00024220 32 0 DebugTest.Person
00000000027f2870 000007ff00024220 32
00000000027f2890 000007ff00024220 32 0 DebugTest.Person
00000000027f2890 000007ff00024220 32
00000000027f28b0 000007ff00024220 32 0 DebugTest.Person
00000000027f28b0 000007ff00024220 32
00000000027f28d0 000007ff00024220 32 0 DebugTest.Person
00000000027f28d0 000007ff00024220 32
00000000027f28f0 000007ff00024220 32 0 DebugTest.Person
00000000027f28f0 000007ff00024220 32
00000000027f2910 000007ff00024220 32 0 DebugTest.Person
00000000027f2910 000007ff00024220 32
00000000027f2a50 000007ff00024220 32 0 DebugTest.Person
00000000027f2a50 000007ff00024220 32
00000000027f2a70 000007ff00024220 32 0 DebugTest.Person
00000000027f2a70 000007ff00024220 32
00000000027f2a90 000007ff00024220 32 0 DebugTest.Person
00000000027f2a90 000007ff00024220 32
So, twenty objects. Let’s have a look at one of the instances. This can be achieved using the !DumpObj command, or !do, in short:
0:007> !do 00000000027f2830
Name: DebugTest.Person
MethodTable: 000007ff00024220
EEClass: 000007ff00132400
Size: 32(0x20) bytes
File: c:\users\hkrause\documents\visual studio 2010\Projects\DebugTest\DebugTest\bin\Debug\DebugTest.exe
Fields:
MT Field Offset Type VT Attr Value Name
000007fef5786870 4000001 8 System.String 0 instance 00000000027f22e8 <Name>k__BackingField
000007fef578c758 4000002 10 System.Int32 1 instance 25 <Age>k__BackingField
The interesting lines here are the last two. They describe the details and the values of the properties. Since I used auto-properties instead of traditional ones they are named <Name>k__BackingField instead something like _Name. Since the name if of type System.String, the value is a reference. So, the next step is to issue a !DumpObj on the value of that field:
0:007> !do 00000000027f22e8
Name: System.String
MethodTable: 000007fef5786870
EEClass: 000007fef530ed58
Size: 38(0x26) bytes
File: C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String: Bob
Fields:
MT Field Offset Type VT Attr Value Name
000007fef578c758 4000103 8 System.Int32 1 instance 6 m_stringLength
000007fef578b298 4000104 c System.Char 1 instance 57 m_firstChar
000007fef5786870 4000105 10 System.String 0 shared static Empty
>> Domain:Value 00000000003ab570:00000000027e1420 <<
The nice thing about !DumpObj is that it prints string values directly to the console. In this case, the value is Bob. Ok, so this is not the instance I’m searching for. Hmm, just to check one instance for its name, I have to issue two !DumpObj commands and do a lot of copy-and-paste to copy the addresses from the console to the command line. And in this example, there are only 20 instances. In the process I debugged, there were much more. There must be a better way to do this. A colleague of mine jumped to help and together we indeed came up with one.
The idea is the following:
- Get a list of the addresses of all Person instances.
- For each entry
- Given the address of the Perons instance, get the value of the <Name>k__BackingField
- Print out the address of the Person instance along with the string value of the <Name>k__BackingField.
The first step is easy: Just execute this line:
!DumpHeap -type Person -short
This command will just dump the addresses of each instance to the console. Iterating through this list is also trivial, thanks to the foreach command:
.foreach(entry {!dumpheap -type Person -short}){.printf "%p:\n", entry;}
For each entry in the list returned by the command between the first pair curly braces, the variable entry is populated with the value and the command sequence between the second pair of curly braces is executed. The example above just prints out the value itself.
Now comes the harder part. We need to figure out the value of the backing field for the Name property. A closer inspection of the !DumpObj result above reveals that the field is stored at memory offset 8 inside of the object. To read that memory directly, the dq command can be used (dq because I’m debugging a 64bit process. Use dd for a 32bit process). Dq prints the memory at the specified address in the form of 64bit hex values to the screen. Since we are only interested in the 64-bit pointer at offset 8 of that object, we issue this command:
0:007> dq 00000000027f2830+8 L1
00000000`027f2838 00000000`027f22e8
The 00000000027f2830+8 specifies the memory address we are interested in. And the L1 specifies that we only want one quad-word. You can see that the result from the dq operation equals the vlaue of the backing field: 00000000027f22e8. We now need a way to dereference that memory address and print the value of the string at that point. At this point it’s good to know how .NET strings are stored in memory: A length indicator (32bit) followed by an unicode-char-array. The result of the DumpObj command on the string instance “Bob” above reveled that the character array starts at offset 0x0c within the string instance. To print the value of the string, the dq command cannot be used because we are interested in the string value itself. For this, the du command can be used. du means: Dump unicode string. To dereference the pointer provided by the dq command, the poi operator can be used. All combined, this resolves to this command:
0:007> du poi(00000000027f2830+8)+c
00000000`027f22f4 "Walter"
Nice, exactly what we wanted. Now we can combine this with the foreach command above:
0:007> .foreach(entry {!dumpheap -type Person -short}){.printf "%p: ", entry; du poi(${entry}+8)+c;.printf "\n"}
00000000027f2588: 00000000`027f293c ""
00000000027f2588: 00000000`027f293c ""
00000000027f25d0: 00000000`027f219c "Alice"
00000000027f25d0: 00000000`027f219c "Alice"
00000000027f2630: 00000000`027f21c4 "Bob"
00000000027f2630: 00000000`027f21c4 "Bob"
00000000027f2650: 00000000`027f21e4 "Carol"
00000000027f2650: 00000000`027f21e4 "Carol"
00000000027f2670: 00000000`027f220c "Chuck"
00000000027f2670: 00000000`027f220c "Chuck"
00000000027f2690: 00000000`027f2234 "Dave"
00000000027f2690: 00000000`027f2234 "Dave"
00000000027f2710: 00000000`027f225c "Eve"
00000000027f2710: 00000000`027f225c "Eve"
00000000027f2730: 00000000`027f227c "Mallory"
00000000027f2730: 00000000`027f227c "Mallory"
00000000027f2750: 00000000`027f22a4 "Peggy"
00000000027f2750: 00000000`027f22a4 "Peggy"
00000000027f2770: 00000000`027f22cc "Victor"
00000000027f2770: 00000000`027f22cc "Victor"
00000000027f2830: 00000000`027f22f4 "Walter"
00000000027f2830: 00000000`027f22f4 "Walter"
00000000027f2850: 00000000`027f231c "Trent"
00000000027f2850: 00000000`027f231c "Trent"
00000000027f2870: 00000000`027f2344 "Charlie"
00000000027f2870: 00000000`027f2344 "Charlie"
00000000027f2890: 00000000`027f236c "Carlos"
00000000027f2890: 00000000`027f236c "Carlos"
00000000027f28b0: 00000000`027f2394 "Arthur"
00000000027f28b0: 00000000`027f2394 "Arthur"
00000000027f28d0: 00000000`027f23bc "Merlin"
00000000027f28d0: 00000000`027f23bc "Merlin"
00000000027f28f0: 00000000`027f23e4 "Paul "
00000000027f28f0: 00000000`027f23e4 "Paul "
00000000027f2910: 00000000`027f2234 "Dave"
00000000027f2910: 00000000`027f2234 "Dave"
00000000027f2a50: 00000000`027f240c "Sue"
00000000027f2a50: 00000000`027f240c "Sue"
00000000027f2a70: 00000000`027f242c "John"
00000000027f2a70: 00000000`027f242c "John"
00000000027f2a90: 00000000`027f2454 "George"
00000000027f2a90: 00000000`027f2454 "George"
Now we can easily spot the instance named Mallory: It’s located at 00000000027f2730 and we can issue a !DumpObj on that address:
0:007> !do 00000000027f2730
Name: DebugTest.Person
MethodTable: 000007ff00024220
EEClass: 000007ff00132400
Size: 32(0x20) bytes
File: c:\users\hkrause\documents\visual studio 2010\Projects\DebugTest\DebugTest\bin\Debug\DebugTest.exe
Fields:
MT Field Offset Type VT Attr Value Name
000007fef5786870 4000001 8 System.String 0 instance 00000000027f2270 <Name>k__BackingField
000007fef578c758 4000002 10 System.Int32 1 instance 34 <Age>k__BackingField
Case closed .
23e87f3d-3cbf-477c-b367-ff28ebda0d3b|7|4.9
Commenter Mike had questions on some details of the article I wrote about Exchange streaming notifications (See the comments in this blog post). In the MSDN article, I wrote this:
Streaming notifications do not work this way. Instead, you can use a combination of notifications and the ExchangeService.SyncFolderItems method. The SyncFolderItems method gives you a snapshot of the changes that occurred in a particular folder. This method also returns a small cookie that contains information about the synchronization state. You can pass this synchronization state to subsequent calls of the SyncFolderItems method to get all changes that have occurred since that synchronization cookie was first issued. You should persist this synchronization state somewhere (either on disk or in a database) so that the application does not need to process all of the items in the folder gain.
I further outlined the steps an application should follow when using streaming subscriptions:
- After your application starts, use the SyncFolderItems method to process all unprocessed items in the folder.
- Create a streaming notification and connection as specified earlier in this article.
- Use the SyncFolderItems method again to process all events that occurred since the last synchronization and before the subscription was activated.
- On each OnNotification event, use the GetItem operation to get additional properties of the specified items.
- Call the SyncFolderItems method periodically to update the local synchronization cookie.
This makes the application seemingly more complex than it needs to be because your application must be able to deal with different change notifications: Those from the streaming subscription and those from SyncFolderItems. Additionally, once a notification is received through the streaming subscription, the synchronization state from the last call to SyncFolderItems is stale. The application must now deal with the situation that the initial call of SyncFolderItems on an application restart might return changes which already have been processed by the application. The application needs to deal with this situation.
Mike asks whether it’s ok to ignore the item ids specified in a notification and just use the SyncFolderItems method on each notification. Using this approach, the synchronization state returned by the SyncFolderItems method would always be current and the whole application design would be more simple.
The answer is: It depends. If you have a low-volume application and your Exchange server is not used at full capacity, this might be ok. If you are writing an in-house application where you know everything about your infrastructure, this might be acceptable. But keep in mind that an application might run several years and the server load of your Exchange Server might go through the roof during the lifetime of your application. And if you are a software developer who sells his application to (hopefully) many customers, you don’t know anything about the performance situation on these servers. So it is best to conform to best practice and use the rules I outlined in the article.
b023a461-8272-479b-ab68-5822199d8788|1|5.0
The mail headers are exposed on the EmailMessage as first class property: InternetMessageHeaders. However, Exchange also stores each mail header in its own property and defines a unique property set for it: PS_INTERNET_HEADERS.
Here is a property definition for a hypothetical custom property:
private static readonly ExtendedPropertyDefinition FrobProperty =
new ExtendedPropertyDefinition(DefaultExtendedPropertySet.InternetHeaders, "X-Frob", MapiPropertyType.String);
If a mail is processed by Exchange with message header like this:
Subject: Test mail
X-Frob: Yes
From: "Henning Krause" <someone@infinitec.de>
To: "Someone <someone@example.local>
Exchange will parse the mail and set the value of the extended property defined above to “Yes”.
Retrieving the custom property is as simple as this:
var item = Item.Bind(service, itemId,
new PropertySet(BasePropertySet.FirstClassProperties, FrobProperty));
The value of the property can then be accessed using this line:
string frobValue;
if (item.TryGetProperty(FrobProperty, out frobValue))
{
Console.Out.WriteLine("The item as a frob value of: {0}", frobValue);
}
You can even search for messages which have the header “X-Frob” set to “Yes”:
var itemId = folder.FindItems(new SearchFilter.IsEqualTo(FrobProperty, "yes"),
new ItemView(10) {PropertySet = PropertySet.IdOnly});
This query will return the first ten messages which have said header set to “Yes”.
Note that Exchange has a limit for named properties which are the result of custom mail headers. You may get one of these entries in your EventLog at some point:
Event ID: 9666
Type: Warning
Category: General
Source: msgidNamedPropsQuotaWarning
Description: The number of named properties created for database "<database name>" is close to quota limit. Current number of named properties: <number of named properties>. Quota limit for named properties: <configured quota>. User attempting to create the named property: <user name>. Named property GUID: <GUID of named property>. Named property name/id: <name of named property>.
Event ID: 9667
Type: Error
Category: General
Source: msgidNamedPropsQuotaError
Description: Failed to create a new named property for database "<database name>" because the number of named properties reached the quota limit (<configured quota>). User attempting to create the named property: <user name>. Named property GUID: <GUID of named property>. Named property name/id: <name of named property>.
See this blog post on how to deal with this issue: http://blogs.technet.com/b/exchange/archive/2010/07/29/3410545.aspx.
d512c9ee-5297-43bb-9109-b57a195503c7|1|4.0
Back in 2008 I wrote an article about accessing the Master Category List using WebDAV. If you are not sure what the Master Category List is, read that article first…
EWS could not be used at that time, since access to the Folder Associated Items via EWS is a Feature of Exchange 2010. So if you are on Exchange 2007, the old article still applies.
The last article contained some code which simplified updating the list. I’ve updated the code and aligned the method naming with that of the EWS managed API.
This is the class diagram for the updated code:
The classes are really easy to use:
var service = new ExchangeService(ExchangeVersion.Exchange2010_SP1) { Credentials = new NetworkCredential("someone@infinitec.de", "password") };
service.AutodiscoverUrl("someone@infinitec.de", url => true);
var list = MasterCategoryList.Bind(service);
foreach (var category in list.Categories)
{
Console.Out.WriteLine("category = {0}", category.Name);
}
The only interesting line in the sample above is line 4. The call to MasterCategoryList.Bind() loads the MasterCategoryList from the users Exchange Mailbox. After that, the name of each console is written to the console.
Adding a new category is equally simple:
var service = new ExchangeService(ExchangeVersion.Exchange2010_SP1) { Credentials = new NetworkCredential("someone@infinitec.de", "password") };
service.AutodiscoverUrl("someone@infinitec.de", url => true);
var list = MasterCategoryList.Bind(service);
list.Categories.Add(new Category("Vacation", CategoryColor.DarkMaroon, CategoryKeyboardShortcut.CtrlF10));
list.Update();
This will add a new category named “Vacation” to the list.
So how does this work? The Master Category List is stored in a hidden message in the calendar folder of a mailbox. EWS in 2010 provides simple access to this message with the UserConfiguration class. This code show how the Master Category List is loaded:
var item = UserConfiguration.Bind(service, "CategoryList", WellKnownFolderName.Calendar,
UserConfigurationProperties.XmlData);
var reader = new StreamReader(new MemoryStream(item.XmlData), Encoding.UTF8, true);
The configuration data is stored in the XmlData property as UTF-8 encoded byte array.
Here is the new code:
3fff28fd-88aa-44a7-9eda-d1fe3673818a|5|5.0
Interesting question in the Exchange development forum today: How do I get the number of unread messages in the inbox folder from a number of mailboxes? The questioner wants to send a text message to each owner of a mailbox notifying him of the number of unread mails in his inbox. The requirement was to do this with PowerShell. So, here is a quick solution for this problem:
param ([string] $inputFile, [System.Management.Automation.PSCredential] $credential)
$ErrorActionPreference = "Stop"
if (($inputFile -eq [String]::Empty) -or (Test-Path $inputFile) -eq $false)
{
throw "Invalid file specified ({0})." -f $inputFile
}
[Reflection.Assembly]::LoadFrom("C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll")
$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService -ArgumentList([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1)
if ($credential -ne $null)
{
$service.Credentials = $credential.GetNetworkCredential()
}
else
{
$service.UseDefaultCredentials = $true;
}
function GetUnreadInboxMails([string] $emailAddress)
{
$service.AutodiscoverUrl($emailAddress, {$true});
$maibox = New-Object Microsoft.Exchange.WebServices.Data.Mailbox($emailAddress)
$folderId = New-Object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox, $mailbox)
$folder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service, $folderId);
$result = New-Object PSObject -Property @{
EmailAddress = $emailAddress
UnreadMailCount= $folder.UnreadCount;
}
return $result;
}
function ProcessResult($entry)
{
"Mailbox: {0}" -f $entry.EmailAddress
"Unread Mails: {0}" -f $entry.UnreadMailCount
"==============================================================================="
}
$addresses = Import-Csv $inputFile
$addresses | % {GetUnreadInboxMails($_.MailAddress)} | % {ProcessResult ($_)}
Just paste this script into an editor and save it as ps1 file.
The script mainly consists of two function: GetUnreadInboxMails and ProcessResult. The former method retrieves the number of unread mails in the inbox of the mailbox of the specified user. The number of unread mails can be retrieved from a Folder instance: The UnreadCount property. The method returns the number of unread mails along with the mail address of the current mailbox. The other important function is the ProcessResult method. In this script it merely dumps the result to the console. The questioner in the post linked above can use this function to call his HTTP service to send a text message.
The script has two parameters:
- The name of a CSV file containing the list of mailboxes to process. The first line should contain the column name “MailAddress” somewhere. The following lines should contain the email address of the mailbox in this column.
- A PSCredential object containing valid credentials to use for the Exchange access.
The credential used for the script needs read access to the inbox folder of each mailbox specified in the file.
fa18b2b9-eacb-44e2-b590-e200b2e98e7f|0|.0