Introduction
This article provides the blueprint of the various mechanisms needed to create a classification and attribution system in Microsoft Outlook 2010. It also provides examples of some common scenarios.
Background
Email has remained unchanged for a long long time. Sure there are
minor labels, categories, rules,
conversations, folders, attachments, and such but nothing truly earth shattering. Self Description of the email is what's missing.
I work at a large
manufacturing company in IT, we have very rigorous processes defined for
execution of projects and programs.
There are articles/documents for defining what to capture in the
software development lifecycle and/or business processes. There are systems in place for capturing that
information. There are reports in place
for aggregating and displaying results of all this. However, the thing that I continue to see,
is that the most valuable information will still show up in Email. The content may eventually make its way to
one of the entrenched systems, but when
I'm looking for something, I hit the email archive first. And from what I can tell, so does everyone else.
So enter Microsoft
Outlook. One of the most feature rich
email clients to date. However, emails are still
very flat, you might place messages in a specific folder, apply a category/label
to it.. But it stops there. Question,
what if I have an email that really belongs in more than one folder? Multiple labels? What if I want to use this email as
reference data for a project?
How? Can these things be accomplished? Simple.
Classification and
Attribution. Creating taxonomy,
relationship, and security model that allows email to finally able to meet that
goal.
So, playing around
with Outlook as a starting point, maybe not getting to my full vision of
replacing social media just yet of taking my Tagging Engine to the Cloud.. But
let's start small.. if I want attribution and classification on an existing
tool, like Outlook.. as that's what I
use daily, I started doing proof of concepts to see how this might work.
What I found is
Outlook is designed with the DNA specifically geared towards doing what I want
to do. I hope Microsoft reads this
article, as they have everything ready to go today, they just need some fine
tuning to make it all work.
Using the code
My example starts with the Microsoft Ribbon example. This allows you to easily add a right click event to email items. Please download and start with this ribbon example from Microsoft: http://msdn.microsoft.com/en-us/library/ee692172.aspx
Modify the explorer.xml file as follows to enable the right click event. You can add as many buttons as you want.
<contextMenu idMso="ContextMenuMailItem">
<button id="MyContextMenuMailItem4"
label="TagIT"
onAction="TagEmailItem"/>
</contextMenu>
</contextMenus>
And as for the admin screen button (jumping ahead, more on that later). You could do something like this:
<ribbon>
<tabs>
<tab id="MyTab"
getVisible="MyTab_GetVisible"
label="TagIT">
<group label="Core" id="MyGroup1" >
<button id="MyButton2"
size="large"
label="TagIT!"
imageMso="Piggy"
screentip="Add Tags to your message"
supertip="Written by Scott Traube"
onAction="LaunchTagging"/>
<button id="MyButton"
size="large"
label="Find your TagIT Emails"
imageMso="FindText"
screentip="Find Emails based on custom tags"
onAction="LaunchSearch"/>
</group>
<group label="Admin" id="MyGroup2" >
<button id="MyButton3"
size="large"
label="Tag Administration"
imageMso="MagicEightBall"
screentip="Modify Class Tags and Attributes"
onAction="LaunchAdmin"/>
</group>
</tab>
</tabs>
</ribbon>
Step 1. How do I tag (my proof of concept application
is called TAG-IT by the way) emails?
Enter MSDN and Google (and Bing of course). I first though, well, just append XML at the
bottom of each email, as I was coming into this with zero Outlook Add-In
exposure . So, XML appended to the end, you can do it, its simple enough,
you get a handle on the message in question, append to the body and save
it.. Done. But when you do that, what if your email is
DOC or HTML type, wow that really screws up the formatting! It can be done though. But maybe this isn't the right
approach. As I peeled back the onion a
bit more, I discovered that each email item stored in Outlook has user defined
attributes. Ahh ha! What I came to discover after some quick
tests, its perfect for what I'm trying to accomplish, it's lightweight, leaves
a very small footprint, is searchable, it out of the box. Ok, so how do I set a User Defined
Attribute? You can start by creating a function defined in the XML above, like:
<button id="MyContextMenuMailItem4" label="TagHello" onAction="helloworld"/>
Now a right click on an email fires this:
public void helloworld(Office.IRibbonControl control)
{
if (control.Context is Outlook.Selection)
{
Outlook.Selection selection =
control.Context as Outlook.Selection;
if (selection.Count == 1)
{
if (selection[1] is Outlook.MailItem)
{
Outlook.MailItem oMailItem = selection[1] as Outlook.MailItem;
oMailItem.UserProperties.Add("TagITUpdated",
Outlook.OlUserPropertyType.olDateTime, false,
Outlook.OlUserPropertyType.olDateTime);
oMailItem.UserProperties["TagITUpdated"].Value = DateTime.Now;
oMailItem.UserProperties.Add("MyCustomValue",
Outlook.OlUserPropertyType.olText, true,
Outlook.OlUserPropertyType.olText);
oMailItem.UserProperties["MyCustomValue"].Value = "Hello World";
oMailItem.Save();
}
}
}
}
That's it. So I've essentially just added the hidden attribute called MyCustomValue
to this specific email and set the value at Hello World. I've also
done a datestamp on the TagITUpdated
attribute.
If I wanted to see the values I've set on an email, I could create another button that's function calls something like this:
public void ShowUserProperties(Outlook.MailItem mail)
{
Outlook.UserProperties mailUserProperties = null;
Outlook.UserProperty mailUserProperty = null;
StringBuilder builder = new StringBuilder();
mailUserProperties = mail.UserProperties;
try
{
for (int i = 1; i <= mailUserProperties.Count; i++)
{
mailUserProperty = mailUserProperties[i];
if (mailUserProperty != null)
{
builder.AppendFormat("Tag: {0} \tValue: {1} \n\r" mailUserProperty.Name,
mailUserProperty.Value);
Marshal.ReleaseComObject(mailUserProperty);
mailUserProperty = null;
}
}
if (builder.Length > 0)
{
System.Windows.Forms.MessageBox.Show(builder.ToString(),
"Hidden Property Values");
}
else
{
System.Windows.Forms.MessageBox.Show("No Hidden Property values found!",
"Notification");
}
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show(ex.Message);
}
finally
{
if (mailUserProperties != null)
Marshal.ReleaseComObject(mailUserProperties);
}
}
Step 2. So if I want to have custom classification and attribution for tagging emails. I need to store that reference data somewhere, right? Hmm, maybe a local Access database, MySQL, XML, flat file? All feasible, but just a lot of overhead.. And what if I use more than one computer that's accessing the same exchange server, if I have a local XML file, it wouldn't be available to this other client without some fancy footwork. So finally, I dug a bit, and found the storage item! Bingo! Again,
Microsoft apparently designed that specifically with me in mind! I can use a hidden storage item to store each class, attribute types, attribute names, pre-defined attribute criteria.. all that, in these storage hidden storage items, that reside on exchange! To do it properly, I would take an XML object, serialize it to a string, and pop it into the Body of a
StorageItem
. Great! So this is how you do that:
private void AddtoStorage(string storageIdentifier, string storageContent)
{
if (!String.IsNullOrEmpty(storageIdentifier) &&
(!String.IsNullOrEmpty(storageContent))
{
Outlook.MAPIFolder folder =
Globals.ThisAddIn.Application.GetNamespace(
"MAPI").GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
Outlook.StorageItem storageitem = folder.GetStorage(
storageIdentifier, Outlook.OlStorageIdentifierType.olIdentifyBySubject);
storageitem.Body = storageContent;
storageitem.Save();
}
}
private string GetfromStorage(string storageIdentifier)
{
if (!String.IsNullOrEmpty(storageIdentifier))
{
Outlook.MAPIFolder folder = Globals.ThisAddIn.Application.GetNamespace(
"MAPI").GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
Outlook.StorageItem storageitem = folder.GetStorage(
storageIdentifier, Outlook.OlStorageIdentifierType.olIdentifyBySubject);
try
{
string bodycontent = storageitem.Body.ToString();
return bodycontent;
}
catch (Exception e)
{
return "";
}
}
else
{
return "";
}
}
Step 3. Now I have a way of storing the definition of my classification, and I also have a way of storing an attribute on an individual email. But how do I tie them
together. Easy, you create a few windows forms in your add-in project! I have 3.
- An administration form, for
creating, modifying your classification. This is launched from the top ribbon.
- A form you can TAG your
individual emails (userproperties)
based on classes and attributes created with your admin form. This is launched when you right click an email.
- A search form to give you
click once attribution from classes and attributes created with your admin
form. This is launched from the top ribbon.
Admin example:
Performing the actual Tagging:
Performing the search:
The full
implementation of doing something like that gets a little complex. So I'll leave an example of launching a
windows form from a right click event.
What I'm doing is getting the StoreID (PST, exchange, wherever this
email current resides) and the EntryID which is the unique identifier at the
time it is in this particular storage location.
The two together is the primary key to that email.
public void LaunchTagging(Office.IRibbonControl control)
{
if (control.Context is Outlook.Selection)
{
Outlook.Selection selection = control.Context as Outlook.Selection;
if (selection.Count == 1)
{
OutlookItem olItem = new OutlookItem(selection[1]);
Form1 formMain = new Form1();
formMain.StorageID = olItem.Parent.StoreID;
formMain.EntryID = olItem.EntryID;
formMain.ShowDialog();
}
}
}
The above just gets the ID's and set's them in this form you're going to open, then it
launches that form.
Note I have this in
the form (ok, you may not need this example, but here it is):
private string entryID = "N/A";
private string storageID = "N/A";
public string EntryID
{
get
{
return entryID;
}
set
{
entryID = value;
}
}
public string StorageID
{
get
{
return storageID;
}
set
{
storageID = value;
}
}
So when the form has launched, I can open that specific email up and apply the classification.
This is how I'd open
it up based on the storageID
and entryID
:
Outlook._Application olApp = new Outlook.ApplicationClass();
Outlook._NameSpace olNS = olApp.GetNamespace("MAPI");
Outlook.MAPIFolder oFolder = olNS.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
Outlook._MailItem oMailItem = (Outlook._MailItem)olNS.GetItemFromID(entryID, storageID);
So oMailItem
is the object I'm going to add attribution to.
And this might be a way you're using something selected on the screen to set the userproperties:
int llindex = listLeft.SelectedIndex;
if (!String.IsNullOrEmpty(lblLeft.Text.ToString() ) && llindex != -1)
{
oMailItem.UserProperties.Add(lblLeft.Text,
Outlook.OlUserPropertyType.olText, true, Outlook.OlUserPropertyType.olText);
oMailItem.UserProperties[lblLeft.Text].Value = listLeft.SelectedItem.ToString();
}
int lcindex = listCenter.SelectedIndex;
if (!String.IsNullOrEmpty(lblCenter.Text.ToString()) && lcindex != -1)
{
oMailItem.UserProperties.Add(lblCenter.Text,
Outlook.OlUserPropertyType.olText, true, Outlook.OlUserPropertyType.olText);
oMailItem.UserProperties[lblCenter.Text].Value = listCenter.SelectedItem.ToString();
}
oMailItem.Save();
Example. I have project called XTR with Envisioning,
Planning, Development, and Sustaining phases.
I also have different types of requirements such as Business, System,
Test, Support. I may also have two
other projects going on at the same time called XT and STX but have the same
requirement types and same phases.. So
in my admin screen, I'd want to create a new project class. It would have three attributes,
ProjectName
,
ProjectPhase
, and RequirementType
.
Further, RequirementType
would have 4 list values. I save all this information into the
storageitem as a serialized XML object ideally.
Now, I have an email
I just received that is for the XTR project, we're in Envisioning phase and the
email is from my development team.
After further investigation, it appears this email has some great
information that I'll need later when I'm writing System Requirement
Specifications. So I right click the
email, it brings up TagIT windows form.
The form pulls the possible classes from the storageitem, I select the
project class with one click, the form now shows me the underlying attributes
for that class, I select ProjectPhase
of Envisioning, and RequirementType
of
System and then click TAGIT/SAVE. 4
quick clicks and this email now is far more usable.
Code wise, If it
were hardcoded, it would look something like this:
oMailItem.UserProperties.Add("ProjectName",
Outlook.OlUserPropertyType.olText, true, Outlook.OlUserPropertyType.olText);
oMailItem.UserProperties["ProjectName"].Value = "XTR";
oMailItem.UserProperties.Add("ProjectPhase",
Outlook.OlUserPropertyType.olText, true, Outlook.OlUserPropertyType.olText);
oMailItem.UserProperties["ProjectPhase"].Value = "Envisioning";
oMailItem.UserProperties.Add("RequirementType",
Outlook.OlUserPropertyType.olText, true, Outlook.OlUserPropertyType.olText);
oMailItem.UserProperties["RequirementType"].Value = "System";
oMailItem.Save();
Be careful, there are some reserved fields in outlook, so if you try to add a UserProperty like Priority, Body, Subject, etc, that already exists, and you'll get an error. So what I do is validate in the admin form to ensure it doesn't exist before saving. So back on topic, At a later point, if
I wanted to see all System level requirement emails for the XTR project, I have
all the information I need, I'd just need a search screen to query based on
XTR, Envisioning, and System.. Now, how
do I find it? Microsoft has that
covered too.. Its out of the box.
I can bring up the
advanced search screen. Goto Fields,
select Advanced Tab. Click Field, then
User Defined Fields. You should actually
find your field named ProjectPhase
and RequirementType
if you're in the right
search context on the correct folder you just added this to. If you were to search on Envisioning, your
result is that email you just tagged.
Advanced Find looks like this:
So on the searchability of that attribute.
Note that for oMailItem.UserProperties.Add
, the third item is a
bool
. This is for addtofolderfields
. Which means, for the folder you're currently
residing in, it will add that attribute, no any value for it, just the
attribute name. This makes it
searchable in that directory. If you
already added that search field before to that folder, it doesn't duplicate it, so call that
as often as you'd like.
Going to the
advanced search each time isn't realistic.
And you don't want to have to manually select them each time, especially
if you have a list pre-defined and available in your hidden storage item. So how to get the search to show up on the
main outlook explorer. First, if you
go to the search screen and do a search as follows: [RequirementType]:=('System')
[ProjectPhase]:=('Envisioning')
The main search
window will provide the results for that call.
But what if we want our application to build that search string and do
the call for us.. But still end up on
the main outlook explorer screen for us?
We can, just like this from our custom search windows form:
Outlook.Explorer explorer = Globals.ThisAddIn.Application.ActiveExplorer();
string searchFor = "[RequirementType]:=('System') [ProjectPhase]:=('Envisioning')";
explorer.CurrentFolder = inbox;
explorer.Display();
explorer.Search(searchFor.Trim(), Outlook.OlSearchScope.olSearchScopeSubfolders);
So your search
screen would have to programmatically set the searchFor
string based on the
criteria pre-populated in your hidden storage item. Just to reiterate, we're never updating
that hidden storage item, we ONLY set that from the admin screen.. Then the
information is static and just referenced as needed from either when you do the tagging or when you perform searches.
Results:
Now, back to this
addtofolderfields
. Note that it only
added it at the level of the folder that email resided in at the time you set
the userattributes. So if you move
that email to another folder, or a PST, the attributes remain, but the
searchability goes away unless they already exist on that folder. So until you add that attribute to the folder to search
on, it won't be found. That is a pain, but the solution
is simple, you loop thru all available
folders and programmatically set the attributes, so all are searchable.. How do you do that you may ask? This is how:
The store
object could represent your default
outlook inbox or a PST.
Outlook.MAPIFolder folder = store.GetRootFolder() as Outlook.Folder;
SetFolderAttributes(folder);
And the
SetFolderAttributes
function basically loops thru all child folders and does
this:
try
{
subfolder.UserDefinedProperties.Add(AttributeName,
Outlook.OlUserPropertyType.olText, Type.Missing, Type.Missing);
}
And this is how you'd loop through folders:
if (folder.Folders.Count > 0)
{
foreach (Outlook.Folder subfolder in folder.Folders)
{
for (int i = 0; i < TagAttributes.Count; i++)
{
th.SetSubfolderAttributes(subfolder, TagAttributes[i].ToString());
}
}
}
Once you've done
this, all your folders are searchable.
And when you search on the hidden attribute, it will find it.
More Example Classes the user could create.
HumanResources (class)
HRTAG (Attribute name)
"Year End Feedback"
"Mid Year Feedback"
"PBA Vacation Related"
HRPerson (Attribute name)
"EmployeeX"
"EmployeeY"
"EmployeeZ"
Assuming I'm a manager, when I get feedback form business partners that is negative
or positive and reflective of an individual contributor's performance, I can Tag it with
that employee's name and year end feedback. At the end of the year,
I can search on those two attributes and have the information I need.
Or perhaps something like this:
CustomerResponse (Class)
CustomerType (Attribute Name)
"Relationship"
"Federal"
"Education"
"Small Business"
"Enterprise"
ResponseType
"shipping question"
"Product Praise"
"Product Defect"
"Order Related"
ResponsePriority
"High"
"Medium"
"Low"
Hope you enjoy this and it spurs some ideas.
Points of Interest
Other ideas of where
this can and should go.
- Classifications set at a
corporate or departmental level.
So they are standardized.
What you'd have to do is determine a location for that, perhaps a
SharePoint list, or the cloud.. then from the client perspective you'd read from the master location and persisted locally on a storageitem.
- Ability to classify an email
as Project Viewable or Departmental Viewable, meaning, once it is classified
by the user that way, its no longer restricted to just that local user or
the users on the TO/CC/BCC, it would aggregate at a higher level, into the
cloud, still authorized within the confines of how it's classed, but an
email has now become something greater, maybe I'm a follower of a
departmental level list, so once someone classifies it that way, I see it
there, everyone else with access to that department sees it too… aka Twitter, Google+ type thinking,
right? But on emails, its a lot
easier on the brain.. See where I'm going with this?
- TagIT engine in the
cloud. Tag news stories, Tag
emails, Tag tweets (well they have hashtags already)... basically, tag anything and everything. Now start
trending the tags and users with them.. Maybe I want
to read news stories tagged similar to how I tagged em.. similar interests. Aka the easiest way to visualize is a Netflix rating system.. And the result? my
news isn't politics politics politics..
Its tailored (cycling, triathlon, mountain biking, techy news). This is
opposite of how things work today, the ad engines aggregate where you go,
what you see, why not just ASK the user!!? (that also goes with my user managed reverse cookie concept, but that's another article). Either way, the quality of all the things behind the scenes could be far
greater quality for the user.. TagIT
History
- Version 1: Posted 3/19/2012.