White magic with Microsoft Exchange (Part I)

Here at Microsoft we have lots of discussion groups, probably in the order of thousands. And I am not sure about how many emails are going daily on these groups but you got the idea. Now, since I always liked to have a peek in everything, I ended up being enlisted in way too many discussion groups. So I get lots of emails in my inbox.

Of course, the immediate solution to this problem is to setup a bunch of Outlook rules that will redirect all emails sent to (or from) a specific group X to a certain folder, probably one that has the same name. But while this solution works for many people, it doesn’t work for me unfortunately. The reason is that Exchange forces a maximum limit of 32KB per folder. So I constantly run out-of-space when setting my Outlook rules. This limitation is not specific to Outlook, it is actually a design detail of Microsoft Exchange (since the server-side rules are stored on the server, obviously). In the Q147298 KB article we get more information about this behavior:

Microsoft Exchange can define rules to automatically operate on messages in the Inbox or in a public folder. There is a 32 kilobyte (KB) "packed data" limit for the rules on each folder, whether it is the Inbox or public folders. The maximum number of rules depends on the size of the rules that are defined, and the amount of data that is required for a rule varies depending on the rule. For example, a rule to move messages into a folder for a single recipient takes about 660 bytes. Therefore, there is no defined limit for the maximum number of rules that can be assigned to a folder. Typically, the limit is between 40 and 50 rules per folder.

So what would be the solution? Fortunately, there is a way out, and this is one of the less known features of Microsoft Exchange: it is possible to define rules for any folder, not only the Inbox folder. How does this help? Let’s assume that I need N rules, and their total size goes beyond 32KB. I can then define M-1 rules on the Inbox folder, and the rest of them in an additional folder named, let’s say “RulesEx”. The rules are defined in this way:

1)      Rules defined in “Inbox”
   a.       Rule 1
   b.      …
   c.       Rule M-1
   d.      Additional rule: <<Move all other emails to “RulesEx”>>
2)      Rules defined in “RulesEx”
   a.       Rule M
   b.      …
   c.       Rule N

If a message arrives in Inbox and there is no rule to process it, it will be automatically redirected to the second folder. The next set of rules will start kicking in, and in the end a matching rule will be found. You can even define several folders with a set of “cascading” rules in this way. Now, the fact that you can define multiple rules on multiple folders is actually a cool feature that someone can use to implement a workflow application. But let’s not go that far right now.

So, it looks like all I have to do is to define more rules on an additional folder. But how I will do that? Outlook won’t let me configure these rules. Fortunately, there is a programmatic way to do that. The hard way is to use MAPI. The easy way is using the RULE.DLL utility provided in the Exchange Developer SDK. You will also need the CDO.DLL 1.21 library (download details: Q171440).

RULE.DLL implements an automation-compatible COM class that uses MAPI under the covers to manipulate rules. It exposes a fairly rich COM object model that allows you to manipulate Exchange rules in any way you like. This object model is well documented on MSDN - you can go here for more information.

Now, let’s do some programming. As a starting point, I presented below some C# code that reads all the rules from your inbox. I hope to follow up with an additional post that will effectively do the magic trick of splitting rules in multiple folders.

In the code below, the first thing to do is to logon into a MAPI session. Do do that, you need to instantiate the CDO._Session class and call the Logon routine. I found the documentation pretty vague on this routine, but I managed to find a combination of default parameters that will succeed the logon. You must have Microsoft Outlook open at that time though; otherwise your Logon will fail.

        _Session session = new SessionClass ();
Object vEmpty = Missing.Value;
session.Logon (vEmpty,vEmpty,false,false,vEmpty,vEmpty,vEmpty);

The next step is to get the collection of rules attached to the Inbox folder. The code looks pretty straightforward. Create a Rules class, and first thing you need to do is to set its Folder property. Then you can do whatever you want: create, delete, enumerate rules, etc. While we are here, note that the Folder class exposes also a rich API that allows you to enumerate your messages in this folder, read their contents etc.

        // Get Inbox.
Folder folder = (Folder)session.Inbox;

// Creating the rules class
IRules rules = new RulesClass ();
rules.Folder = folder;

One thing to note is that the rules are organized in a highly hierarchical manner, so I choose XML to display these rules to the console.

 
 
using System; 
using System.Reflection; 
using System.Collections; 
using System.Xml; 
using MAPI; 
using MSExchange; 
 
 
internal class RulesMgr 
{ 
    private XmlTextWriter writer; 
 
    public RulesMgr() 
    { 
        // Create the XML writer 
        writer = new XmlTextWriter(Console.Out); 
        writer.Formatting = Formatting.Indented; 
    } 
 
    [STAThread] 
    static void Main(string[] args) 
    { 
        // Logging on... 
        _Session session = new SessionClass(); 
        Object vEmpty = Missing.Value; 
        session.Logon(vEmpty,vEmpty,false,false,vEmpty,vEmpty,vEmpty); 
 
        // Get Inbox. 
        Folder folder = (Folder)session.Inbox; 
     
        // Creating the rules class 
        IRules rules = new RulesClass(); 
        rules.Folder = folder; 
 
        RulesMgr mgr = new RulesMgr(); 
        mgr.DumpRules(rules); 
 
        // Log off ... 
        session.Logoff(); 
    } 
 
     
    ///  
    /// Dump all rules 
    ///  
    ///  
    public void DumpRules(IRules rules) 
    { 
        // Writing rules to the XML 
        writer.WriteStartElement("Rules"); 
 
        foreach (IRule rule in rules) 
            DumpRule(rule); 
 
        writer.WriteEndElement(); 
    } 
 
 
    ///  
    /// Dumps a rule 
    ///  
    ///  
    private void DumpRule(IRule rule) 
    { 
        writer.WriteStartElement("Rule"); 
 
        WriteAttrib("name", rule.Name); 
        WriteAttrib("index", rule.index); 
        WriteAttrib("exitLevel", rule.Level); 
        WriteAttrib("state", rule.State); 
 
        // Print actions 
        foreach (IAction action in (IActions)rule.Actions) 
            DumpAction(action); 
 
        // Print the condition 
        ICondition condition = (ICondition)rule.Condition; 
        if (condition != null) 
            DumpCondition(condition); 
 
        writer.WriteEndElement(); 
    } 
 
 
    ///  
    /// Dumps an action 
    ///  
    ///  
    private void DumpAction(IAction action) 
    { 
        writer.WriteStartElement("Action"); 
 
        WriteAttrib("type", action.ActionType); 
        switch(action.ActionType) 
        { 
            case MSExchange.ACTION_TYPES.ACTION_MOVE: 
            { 
                Folder folder = (Folder)action.Arg; 
                WriteAttrib("moveToFolder", folder.Name); 
                break; 
            } 
            case MSExchange.ACTION_TYPES.ACTION_COPY: 
            { 
                Folder folder = (Folder)action.Arg; 
                WriteAttrib("copyToFolder", folder.Name); 
                break; 
            } 
            case MSExchange.ACTION_TYPES.ACTION_REPLY: 
            { 
                WriteAttrib("replyMessage", action.Arg); 
                break; 
            } 
            case MSExchange.ACTION_TYPES.ACTION_FORWARD: 
            { 
                WriteAttrib("recipientList", action.Arg); 
                break; 
            } 
            case MSExchange.ACTION_TYPES.ACTION_DELEGATE: 
            { 
                WriteAttrib("recipientList", action.Arg); 
                break; 
            } 
            case MSExchange.ACTION_TYPES.ACTION_BOUNCE: 
            { 
                WriteAttrib("reason", action.Arg); 
                break; 
            }         
            case MSExchange.ACTION_TYPES.ACTION_TAG: 
            { 
                WriteAttrib("property", action.Arg); 
                break; 
            }    
            default: 
                break; 
        } 
 
        writer.WriteEndElement(); 
    } 
 
         
         
    ///  
    /// Dumps a condition recursively 
    ///  
    ///  
    private void DumpCondition(ICondition condition) 
    { 
        writer.WriteStartElement("Condition"); 
 
        WriteAttrib("type", condition.Type); 
        switch(condition.Type) 
        { 
            case MSExchange.CONDITION_TYPES.R_BITMASK: 
            { 
                IBitmaskCondition bitmaskCondition = (IBitmaskCondition)condition; 
                WriteAttrib("operator", bitmaskCondition.Operator); 
                WriteAttrib("property", bitmaskCondition.PropertyTag); 
                WriteAttrib("value", bitmaskCondition.Value); 
                break; 
            } 
            case MSExchange.CONDITION_TYPES.R_COMMENT: 
            { 
                ICommentCondition commentCondition = (ICommentCondition)condition; 
                foreach(IPropVal propVal in commentCondition) 
                    DumpPropVal(propVal); 
                break; 
            } 
            case MSExchange.CONDITION_TYPES.R_COMPAREPROPS: 
            { 
                IComparePropsCondition comparePropsCondition = (IComparePropsCondition)condition; 
                WriteAttrib("operator", comparePropsCondition.Operator); 
                WriteAttrib("propertyTag1", comparePropsCondition.PropertyTag1); 
                WriteAttrib("propertyTag2", comparePropsCondition.PropertyTag2); 
                break; 
            } 
            case MSExchange.CONDITION_TYPES.R_CONTENT: 
            { 
                IContentCondition contentCondition = (IContentCondition)condition; 
                WriteAttrib("operator", contentCondition.Operator); 
                WriteAttrib("propertyType", contentCondition.Operator); 
 
                IPropVal propVal = (IPropVal)contentCondition.Value; 
                DumpPropVal(propVal); 
                break; 
            } 
            case MSExchange.CONDITION_TYPES.R_EXISTS: 
            { 
                IExistsCondition existsCondition = (IExistsCondition)condition; 
                WriteAttrib("propertyTag", existsCondition.PropertyTag); 
                break; 
            } 
            case MSExchange.CONDITION_TYPES.R_LOGICAL: 
            { 
                ILogicalCondition logicalCondition = (ILogicalCondition)condition; 
                WriteAttrib("operator", logicalCondition.Operator); 
                foreach(ICondition innerCondition in logicalCondition) 
                    DumpCondition(innerCondition); 
                break; 
            } 
            case MSExchange.CONDITION_TYPES.R_PROPERTY: 
            { 
                IPropertyCondition propertyCondition = (IPropertyCondition)condition; 
                WriteAttrib("operator", propertyCondition.Operator); 
                WriteAttrib("propertyTag", propertyCondition.PropertyTag); 
                IPropVal propVal = (IPropVal)propertyCondition.Value; 
                DumpPropVal(propVal); 
                break; 
            } 
            case MSExchange.CONDITION_TYPES.R_SIZE: 
            { 
                ISizeCondition sizeCondition = (ISizeCondition)condition; 
                WriteAttrib("operator", sizeCondition.Operator); 
                WriteAttrib("propertyTag", sizeCondition.PropertyTag); 
                WriteAttrib("size", sizeCondition.Size); 
                break; 
            } 
            case MSExchange.CONDITION_TYPES.R_SUB: 
            { 
                ISubCondition subCondition = (ISubCondition)condition; 
                WriteAttrib("operator", subCondition.Operator); 
                DumpCondition((ICondition)subCondition.Condition); 
                break; 
            } 
        }     
 
        writer.WriteEndElement(); 
    } 
 
 
    ///  
    /// Dumps a tag-value pair 
    ///  
    ///  
    private void DumpPropVal(IPropVal propVal) 
    { 
        writer.WriteStartElement("PropVal"); 
        WriteAttrib("tag", propVal.Tag); 
        WriteAttrib("value", propVal.Value); 
        writer.WriteEndElement(); 
    } 
 
    ///  
    /// Writes an XML attribute 
    ///  
    ///  
    ///  
    private void WriteAttrib(string name, object attrib) 
    { 
        writer.WriteAttributeString(name, attrib.ToString()); 
    } 
 
} 
 

And here is a fragment of the generated rules from my Inbox:

Happy programming!

 

FAQ:

Q: How do I obtain RULE.DLL?

A: I am not sure where you can get RULE.DLL. I got it in compiled form from the CD of the book “Programming with Outlook and Exchange”, 2nd edition, 2000, so you are lucky if you happen to have that book. But surprisingly, we can even find the source code of RULE.DLL in some older Exchange Developer SDKs. So you can compile it yourself though Visual C++.

Q: I get some errors when compiling the C# sample code above:
 “The type or namespace name ‘MAPI’ could not be found … ” or
 “The type or namespace name ‘CDO’ could not be found … ”

A: If you want to compile this C# code in Visual Studio, you need first to add RULE.DLL and CDO.DLL into the “references” section.

1) Make sure that both rule.dll and cdo.dll are present in the system and registered correctly.
2) Go to the “Refernces” folder, right-click, select “Add Reference…” and select the “COM” tab.
3) Select the “Microsoft CDO 1.21 library” component.
4) Select the “Microsoft Exchange SDK 5.0 Rules 1.0” component.

Also, be careful to not confuse the CDO.DLL used here with CDOSYS.DLL which is a completely different component, mentioned “Microsoft CDO for Windows 2000 Library” in the components list.