Outlook NewMail unleashed: writing a working solution (C# example)
In the previous part we looked at using Extended MAPI to handle new mails in Outlook and despite the fact that is does a pretty decent job of it, it still has some serious limitations.
In this, the fourth and final part of the series, we will write our own solution to the problem. Before we start I want to give a HUGE thank you to Renat, this post would not have been possible without his assistance and advice.
The add-in we’re about to write will solve the NewMail challenge in a few ways. First, we’ll write logic that will scan all mail folders and check for any new e-mails. This logic will be fired on Outlook start up using the AddinStartupComplete event and after the Outlook synchronization finishes, the SyncEnd event will be used for this. The NewMailEx event will also be making an appearance and despite its limitations it can still be useful to catch some incoming e-mails.
We will save a last received date and time to know which e-mails are new. It is important to save the last received date for each profile because the user can receive an e-mail at different times for different profiles. We will also keep a list of e-mails that was already received, using this list we can check any new e-mail and whether we already processed them. If the new e-mail is not already in our list we will save it, and as soon as we’ve processed it, it can be removed from the list.
Right, enough talking, let’s get down to business. Same drill as always, start by creating a new ADX COM Add-in in Visual Studio.
This solution will work in all version of Microsoft Outlook so select Microsoft Office 2000 as the minimum supported Office version.
On the next screen, only choose Microsoft Outlook in the list of supported applications.
Create a new class called SyncTime. This class will be used to save the last received time. The code for the class looks like this:
public class SyncTime { public string AccountID; public DateTime LatestTime; public SyncTime() { } }
Next, we need to create a class to store the e-mail details called SyncItem:
public class SyncItem { public string EntryID; public DateTime ReceivedTime; public string SenderSubject; public string AccountID; public SyncItem() { } }
We need to store the information about the e-mails and times somewhere, for this we will create a local xml file. In order to make it easier to work with this file, let’s create a class called SyncItems. This class will have a list of SyncTime items, one entry for each account in Outlook as well as a list of SyncItem items containing entries for each new email.
public class SyncItems { public List<SyncItem> Items; public List<SyncTime> SyncTimes; public delegate void NewMailReceivedHandler(object sender, SyncItem e); public event NewMailReceivedHandler NewMailReceived; public SyncItems() { Items = new List<SyncItem>(); SyncTimes = new List<SyncTime>(); } private string DirectoryName { get { string AppDataFolder = Path.Combine(Environment.GetFolderPath( Environment.SpecialFolder.ApplicationData), "NewMailUnleashed"); return AppDataFolder; } } public string FileName { get { return Path.Combine(DirectoryName, "NewMails.xml"); } } private bool CheckFile() { try { if (!Directory.Exists(DirectoryName)) { Directory.CreateDirectory(DirectoryName); } if (!File.Exists(FileName)) { XmlDocument xmlDoc = new XmlDocument(); XmlDeclaration xmlDeclaration = xmlDoc.CreateXmlDeclaration("1.0", "UTF-8", null); XmlElement xmlElement = xmlDoc.CreateElement("SyncDetails"); xmlDoc.AppendChild(xmlElement); xmlDoc.InsertBefore(xmlDeclaration, xmlElement); xmlDoc.Save(FileName); if (!File.Exists(FileName)) return false; } } catch { return false; } return true; } public void Load() { Items.Clear(); if (CheckFile()) { XmlDocument xmlDoc = new XmlDocument(); if (xmlDoc != null) { xmlDoc.Load(FileName); XmlNode rootNode = xmlDoc.DocumentElement; foreach (XmlNode node in rootNode) { if (node.Name == "synctimes") { foreach (XmlNode ItemNode in node) { SyncTime NewItem = new SyncTime(); if ((ItemNode.InnerText != null) && (ItemNode.InnerText != string.Empty)) { NewItem.LatestTime = Convert.ToDateTime(ItemNode.InnerText); } if (ItemNode.Attributes["accountid"] != null) { NewItem.AccountID = ItemNode.Attributes["accountid"].Value; } SyncTimes.Add(NewItem); NewItem = null; } } else if (node.Name == "syncitems") { foreach (XmlNode ItemNode in node) { SyncItem NewItem = new SyncItem(); NewItem.EntryID = ItemNode.InnerText.Trim(); if (ItemNode.Attributes["receivedtime"] != null) { NewItem.ReceivedTime = Convert.ToDateTime( ItemNode.Attributes["receivedtime"].Value); } if (ItemNode.Attributes["sendersubj"] != null) { NewItem.SenderSubject = ItemNode.Attributes["sendersubj"].Value; } if (ItemNode.Attributes["accountid"] != null) { NewItem.AccountID = ItemNode.Attributes["accountid"].Value; } Items.Add(NewItem); NewItem = null; } } } rootNode = null; xmlDoc = null; } } } public void Save() { if (CheckFile()) { XmlDocument xmlDoc = new XmlDocument(); if (xmlDoc != null) { XmlDeclaration xmlDeclaration = xmlDoc.CreateXmlDeclaration("1.0", "UTF-8", null); XmlElement rootElement = xmlDoc.CreateElement("syncdetails"); xmlDoc.AppendChild(rootElement); XmlElement syncTimesElement = xmlDoc.CreateElement("synctimes"); foreach (SyncTime item in SyncTimes) { XmlElement syncTimeElement = xmlDoc.CreateElement("synctime"); syncTimeElement.InnerText = item.LatestTime.ToString(); XmlAttribute accountIdAttribute = xmlDoc.CreateAttribute("accountid"); accountIdAttribute.Value = item.AccountID; syncTimeElement.Attributes.Append(accountIdAttribute); syncTimesElement.AppendChild(syncTimeElement); } rootElement.AppendChild(syncTimesElement); XmlElement ItemsElem = xmlDoc.CreateElement("syncitems"); foreach (SyncItem item in Items) { XmlElement syncItemElement = xmlDoc.CreateElement("item"); XmlAttribute receivedTimeAttribute = xmlDoc.CreateAttribute("receivedtime"); receivedTimeAttribute.Value = item.ReceivedTime.ToString(); syncItemElement.Attributes.Append(receivedTimeAttribute); XmlAttribute accountIdAttribute = xmlDoc.CreateAttribute("accountid"); accountIdAttribute.Value = item.AccountID; syncItemElement.Attributes.Append(accountIdAttribute); XmlAttribute subjectAttribute = xmlDoc.CreateAttribute("sendersubj"); subjectAttribute.Value = item.SenderSubject; syncItemElement.Attributes.Append(subjectAttribute); syncItemElement.InnerText = item.EntryID; ItemsElem.AppendChild(syncItemElement); } rootElement.AppendChild(ItemsElem); xmlDoc.InsertBefore(xmlDeclaration, rootElement); xmlDoc.Save(FileName); xmlDoc = null; } } } public bool Contains(SyncItem item) { foreach (SyncItem syncItem in Items) { if (item.EntryID == syncItem.EntryID) return true; if ((item.ReceivedTime == syncItem.ReceivedTime) && (item.SenderSubject == syncItem.SenderSubject)) return true; } return false; } public void Add(SyncItem itemToAdd) { if (!Contains(itemToAdd)) { Items.Add(itemToAdd); if (NewMailReceived != null) { NewMailReceived(this, itemToAdd); } } } public void Remove(string entryID) { int RemovePosition = -1; foreach (SyncItem CurrentItem in Items) { if (CurrentItem.EntryID == entryID) { RemovePosition = Items.IndexOf(CurrentItem); break; } } if (RemovePosition != -1) { Items.RemoveAt(RemovePosition); } } public void RemoveByTime(DateTime dateTime, string accountID) { dateTime = dateTime.AddMinutes(-1); List<SyncItem> ItemsToRemove = new List<SyncItem>(); foreach (SyncItem CurrentItem in Items) { if ((accountID == CurrentItem.AccountID) && (CurrentItem.ReceivedTime < dateTime)) { ItemsToRemove.Add(CurrentItem); } } foreach (SyncItem CurrentItem in ItemsToRemove) { Items.Remove(CurrentItem); } } public void InitializeSyncTimesAccounts(DateTime time, Outlook._Application OutlookApp) { SyncTimes.Clear(); Outlook.NameSpace nameSpace = null; Outlook.Folders profileFolders = null; try { if (OutlookApp == null) return; nameSpace = OutlookApp.GetNamespace("MAPI"); if (nameSpace != null) { profileFolders = nameSpace.Folders; int numberOfProfiles = profileFolders.Count; for (int i = 1; i <= numberOfProfiles; i++) { Outlook.MAPIFolder currProfile = profileFolders.Item(i); if (currProfile != null) { SyncTime NewTime = new SyncTime(); NewTime.AccountID = currProfile.EntryID; NewTime.LatestTime = time; SyncTimes.Add(NewTime); Marshal.ReleaseComObject(currProfile); } } } } finally { if (nameSpace != null) Marshal.ReleaseComObject(nameSpace); if (profileFolders != null) Marshal.ReleaseComObject(profileFolders); } } public void SetSyncTime(DateTime time, string accountID) { foreach (SyncTime item in SyncTimes) { if (item.AccountID == accountID) { item.LatestTime = time; RemoveByTime(time, accountID); break; } } Save(); } }
The class has a number of methods. The most important is the Load method, which will check whether the xml file exist and will also create a file if it does not exist. If the file exists it will load the data in the file into the class’ Items and SyncTimes lists. Opposite to the Load method is the Save method, which in turn will save the data in the Items and SyncItems lists to the xml file. One last method in this class is InitializeSyncTimesAccounts; it sets the last received dates for each profile in Outlook. The class also has an event which will be raised as soon as a new e-mail is added to the Items collection.
With the required classes in place, switch to the AddinModule’s designer and drag a new Timer component called mailCheckTimer onto the designer surface. Set its Enabled property to True and its Interval property to 1000.
Next, select the AddinModule designer and double-click in the AddinStartupComplete event to generate a new event handler.
Add the following code to the event handler:
private SyncItems syncItems = new SyncItems(); private void AddinModule_AddinStartupComplete(object sender, EventArgs e) { syncItems.NewMailReceived += syncItems_NewMailReceived; if (!File.Exists(syncItems.FileName)) { syncItems.InitializeSyncTimesAccounts(DateTime.Now, this.OutlookApp); syncItems.Save(); } else { syncItems.Load(); } mailCheckTimer.Start(); }
Declare an event handler for the SyncItems class’s NewMailReceived event; Visual Studio will automatically generate the code for the event handler for you if you press TAB. In the event handler code you can put the logic to handle the new e-mail, for example:
void syncItems_NewMailReceived(object sender, SyncItem e) { // Use this event to handle all incoming e-mails System.Diagnostics.Debug.WriteLine( String.Format("{0} ->{1}{2}", e.ReceivedTime, e.EntryID, Environment.NewLine)); }
We will assume that if the xml file does not exist, that this is the first time the add-in is run and set the last received date to the current system date. If the file does exist, we load it and lastly we start the timer.
Generate an event handler for the timer’s Tick event and add the following code:
private void mailCheckTimer_Tick(object sender, EventArgs e) { this.SendMessage(0x04001, IntPtr.Zero, IntPtr.Zero); }
As you can see we’re using the SendMessage method provided by Add-in Express. You can add an event handler for the OnSendMessage event in the same way we’ve added it for the AddInStartupComplete event and add the following code to it:
private void AddinModule_OnSendMessage(object sender, ADXSendMessageEventArgs e) { if (e.Message == 0x04001) { mailCheckTimer.Stop(); LoopThroughAccountFolders(); } }
In these methods, we loop through all the Outlook folder recursively using the LoopThroughtAccountFolders and ScanFolder methods. We then write the entryid of the new e-mails to a text file. You can do any manner of processing at this point. After we’ve processed the new e-mail they are removed from the xml file. The code for the LoopThroughtAccountFolders and ScanFolder methods looks like this:
private void LoopThroughAccountFolders() { Outlook.NameSpace nameSpace = OutlookApp.GetNamespace("MAPI"); Outlook.Folders accountFolders = nameSpace.Folders; for (int i = 1; i < = accountFolders.Count; i++) { Outlook.MAPIFolder accountFolder = accountFolders.Item(i); SyncTime syncTime = syncItems.SyncTimes.Find( s => s.AccountID == accountFolder.EntryID); if (syncTime != null) { LastReceivedDate = DateTime.MinValue; ScanFolder(accountFolder, syncTime.LatestTime, accountFolder.EntryID); } if (LastReceivedDate > DateTime.MinValue) { syncItems.SetSyncTime(LastReceivedDate, accountFolder.EntryID); syncItems.Save(); } if (accountFolder != null) Marshal.ReleaseComObject(accountFolder); } if (accountFolders != null) Marshal.ReleaseComObject(accountFolders); if (nameSpace != null) Marshal.ReleaseComObject(nameSpace); } private void ScanFolder(Outlook.MAPIFolder folder, DateTime receivedTime, string accountId) { if (folder.DefaultItemType == Outlook.OlItemType.olMailItem) { Outlook.Items folderItems = folder.Items; Outlook.Items filteredItems = folderItems.Restrict( String.Format("[ReceivedTime] >= '{0}'", receivedTime.AddMinutes(-1).ToString("g"))); Marshal.ReleaseComObject(folderItems); for (int i = 1; i < = filteredItems.Count; i++) { object outlookItem = filteredItems.Item(i); if (!IsSentItem(outlookItem)) { if (outlookItem is Outlook.MailItem) { Outlook.MailItem outlookMail = (Outlook.MailItem)outlookItem; SyncItem syncItem = new SyncItem(); syncItem.EntryID = outlookMail.EntryID; syncItem.SenderSubject = outlookMail.Subject; syncItem.ReceivedTime = outlookMail.ReceivedTime; syncItem.AccountID = accountId; syncItems.Add(syncItem); if (syncItem.ReceivedTime > LastReceivedDate) LastReceivedDate = syncItem.ReceivedTime; } } Marshal.ReleaseComObject(outlookItem); } } Outlook.Folders subFolders = folder.Folders; for (int i = 1; i < = subFolders.Count; i++) { Outlook.MAPIFolder folderToScan = subFolders.Item(i); ScanFolder(folderToScan, receivedTime, accountId); Marshal.ReleaseComObject(folderToScan); } Marshal.ReleaseComObject(subFolders); }
So far the code will take care of any new e-mails received when Outlook starts-up. We now need to add logic to make sure we scan for new e-mails after each e-mail synchronization or rather after each Send/Receive. To do this, switch to the AddinModule designer surface and add a new Microsoft Outlook events component. Generate a new event handler for the SyncEnd event and change its code to:
private void adxOutlookEvents_SyncEnd(object sender, object syncObject) { mailCheckTimer.Start(); }
All the code does, is to start our timer, which will wait for 1 second and then scan the folders again. Finally to make sure we do not miss any e-mails, add an event handler for the NewMailEx event and add the following code to it:
private void adxOutlookEvents_NewMailEx(object sender, string entryIDCollection) { Outlook.NameSpace nameSpace = null; object outlookItem = null; try { nameSpace = OutlookApp.GetNamespace("MAPI"); string[] entryIds = entryIDCollection.Split(','); for (int i = 0; i < entryIds.Length; i++) { outlookItem = nameSpace.GetItemFromID(entryIds[i]); if (outlookItem != null) { if (outlookItem is Outlook.MailItem) { Outlook.MailItem outlookMail = (Outlook.MailItem)outlookItem; SyncItem syncItem = new SyncItem(); syncItem.EntryID = outlookMail.EntryID; syncItem.SenderSubject = outlookMail.Subject; syncItem.ReceivedTime = outlookMail.ReceivedTime; syncItem.AccountID = GetItemAccountID(outlookItem); syncItems.Add(syncItem); syncItems.Save(); } Marshal.ReleaseComObject(outlookItem); } } } finally { if (nameSpace != null) Marshal.ReleaseComObject(nameSpace); } }
This will process any e-mails received in the NewMailEx event, check if it is already in the xml list of e-mails and add it if it is not.
Phew, that took some doing and as you can see there are various scenarios you would need to take into consideration in order to effectively handle new e-mails in Outlook. The big benefit of this approach is it will work in all versions of Outlook and is certainly likely to work in any future versions of Microsoft Outlook.
Thank you very much for reading! And again a big thank you to Renat and Dmitry for their help and advice during this series.
Until next time. Keep coding!
Available downloads:
This sample add-on was developed using Add-in Express 2010 for Office .net:
Sample add-on in C#
Outlook NewMail unleashed:
- Part 1. Outlook NewMail event – the challenge
- Part 2. Outlook NewMail solution options
- Part 3. NewMail event and Extended MAPI
- Part 4. Writing your custom solution (C# sample)
11 Comments
What happens with the performance? it seems that the folders are scanned in the same thread, i assume Outlook will not respond during the scanning..
Hi Marco,
Scanning should be done on the main thread to avoid Outlook crashing. We haven’t seen any perfomance knocks during our tests.If you have *a lot* of folders I guess you might have some perfomance issues. However, you can then always write logic to only scan certain folders.
Thanks for your comment!
Hello Pieter,
I also have the feeling that it could freeze Outlook. Especially if customer have a lot of folders (or Shared Folders?)
Do you think it’s possible to do this job (scanning folders) in the background using extended MAPI ?
I already do some background processing on folder using Redemption and after some tweaking (check that MAPI is initialized once per thread, re-use the same thread to avoid a bug after several hundred of MAPI initialization, etc.) it seems to works fine.
Thank you for this last part, it helps a lot.
Btw, I found Microsoft very bad on this point, as I don’t understand how there is no simple way to get this “NewMail” event correctly.
Hi Fabske!
I guess if you have a lot of folders it could cause some performance problems. In this case I would write some logic to specify which folders to scan.
Be careful with scanning folders on another thread as it could cause Outlook to crash. However the way you’ve described using Redemption might work with extended MAPI, as long as you approach with caution :)
It is a shame MS could not give us a more solid solution, but you have to have some sympathy, there are so many ways for e-mail to get delivered to Outlook, it can’t be easy to write an all encompassing event :)
Thanks for your comment.
Thanks very much for this. I have implemented it and it seems OK but the SyncEnd event is never firing. I have adapted it for vb but it has the correct Handles statement.
Private Sub adxOutlookEvents_SyncEnd(sender As Object, syncObject As Object) Handles adxOutlookEvents.SyncEnd
mailCheckTimer.Start()
End Sub
The sync start event does not seem to fire either. (I know it is not needed for your solution)
My dev machine runs Outlook 2013 with an Office 365 account. Would this effect the sync event? I cannot find any ADX documentation on this event.
Hi Mike,
Due to an issue that was reported at : https://social.msdn.microsoft.com/Forums/office/en-US/7a20664c-7650-4d61-9d5f-13f1d929a8cd/outlook-2013-automatic-sendreceive-doesnt-work-with-the-sample-addin-below?forum=outlookdev.
The SyncStart, SyncProgress, SyncEnd, and SyncError events don’t occur because they are disconnected in Add-in Express. However in the latest build(v7.7.4087) there is a way to handle these events by setting the SyncObjects property to true. This property is located under the HandleEvents property.
Hope this helps!
I cannot seem to get your example to work, as my system does not recognize item(i) under LoopThroughAccountFolders:
Outlook.MAPIFolder accountFolder = accountFolders.Item(i);
It also does not recognize item{i} under ScanFolder(Outlook.MAPIFolder folder, DateTime receivedTime, string accountId):
object outlookItem = filteredItems.Item(i);
What am I missing?
Hi Joseph,
I think these issues are caused by using different Primary Interop Assemblies (PIAs) in our sample and your project. Please try the following code:
Outlook.MAPIFolder accountFolder = accountFolders[i];
object outlookItem = filteredItems[i];
That worked thank you.
I have the latest Add-In Express, and it seems that the SyncStart, SyncProgress, SyncEnd, and SyncError events are still not firing.
What do I need to do to make this work?
Hi Joseph,
You need to add the ADXOutlookAppEvents component to your add-in module designer and change the HandleEvents property.