I have written some C# code that reads emails from Microsoft Outlook and writes the emails into a MongoDB. Specifically, my code puts an email into a MongoDB-compatible BsonDocument, and then it inserts the BsonDocument into a MongoDB collection.
In the C# interface to Outlook, you can open a Folder and call Folder.Items. This returns an object of type Items. When you iterate over an Items object, you encounter multiple object types, including MailItem and MeetingItem. Most of the fields in MailItem and MeetingItem are the same (e.g. Body, Subject, etc). In my code example below, I have separate functions to process MailItem and MeetingItem. However, this leads to code duplication.
So, my question is: is it there a way to write a function that is agnostic to whether the input is of type MailItem or MeetingItem?
Minimal code example
In my program, I import these things:
using Bson = MongoDB.Bson;
using Driver = MongoDB.Driver;
using Outlook = Microsoft.Office.Interop.Outlook;
And, the next three snippets comprise the body of the program:
namespace ReadEmail{
class Program{
public static Bson.BsonDocument convertMailItemToBson(Outlook.MailItem item){
Bson.BsonDocument retval = new Bson.BsonDocument();
retval.Add("Body", item.Body);
retval.Add("Subject", item.Subject);
return retval;
}
...and the same code a second time for MeetingItem:
public static Bson.BsonDocument convertMeetingItemToBson(Outlook.MeetingItem item){
Bson.BsonDocument retval = new Bson.BsonDocument();
retval.Add("Body", item.Body);
retval.Add("Subject", item.Subject);
return retval;
}
Finally, a main function to connect to Outlook, connect to MongoDB, and feed emails and meetings into the above functions:
static void Main(string[] args){
Outlook.Application outlookApplication = new Outlook.Application();
Outlook.NameSpace outlookNamespace = outlookApplication.GetNamespace("MAPI");
Outlook.MAPIFolder folder = outlookNamespace.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
var dbClient = new Driver.MongoClient("mongodb://localhost");
var database = dbClient.GetDatabase("myDb");
var collection = database.GetCollection<Bson.BsonDocument>("myCollection1");
Outlook.Items items = folder.Items;
foreach (var item in items){
if (item is Outlook.MailItem mailItem){
Bson.BsonDocument iBson = convertMailItemToBson(mailItem);
collection.InsertOne(iBson);
}
else if (item is Outlook.MeetingItem meetingItem){
Bson.BsonDocument iBson = convertMeetingItemToBson(meetingItem);
collection.InsertOne(iBson);
}
}
}
}
}
The full program for the above code is in this gist. Note that in the full program, we aren't just reading 2 fields (Subject and Body). Instead, in the full program, we are reading 25 fields.
One thing I tried that hasn't yet worked
For our purposes, the only difference between MailItem and MeetingItem that matters is that MeetingItem doesn't have the To, CC, or BCC fields. So, I thought I could create a templatized function as follows:
public static Bson.BsonDocument convertItemToBson<T>(T item) where T : Outlook.MailItem, Outlook.MeetingItem
{
Bson.BsonDocument retval = new Bson.BsonDocument();
retval.Add("Body", item.Body);
retval.Add("Subject", item.Subject);
return retval;
}
However, I get the following compiler error:
CS0229 Ambiguity between '_MailItem.Body' and '_MeetingItem.Body'.
Note that out of the 25 fields in the full program, all of them have this error. Also note that for my full templatized version, I removed the usage of the To, CC, and BCC fields, because these fields exist in MailItem but not MeetingItem.
Recap
To summarize the question: In the above code, is there a way to combine convertMailItemToBson() and convertMeetingItemToBson() into one function that can accept either a MailItem or a MeetingItem as input?
You can just pass the parameters in method.
public static Bson.BsonDocument convertItemToBson(string body, string subject){
Bson.BsonDocument retval = new Bson.BsonDocument();
retval.Add("Body", body);
retval.Add("Subject", subject);
return retval;
}
and while calling you can call like below.
foreach (var item in items){
if (item is Outlook.MailItem mItem){
Bson.BsonDocument iBson = convertItemToBson(mItem.Body, mItem.Subject);
collection.InsertOne(iBson);
}
else if(item is Outlook.MailItem mItem){
Bson.BsonDocument iBson = convertItemToBson(mItem.Body, mItem.Subject);
collection.InsertOne(iBson);
}
}
There could be many possibilities. I think this would work. I have not tested the code as i don't have all code. Feel free to comment.
Pass the item as a generic object and use reflection to read the property values.
Related
I have gone through many links already but none of them seems to be working. My problem is that in an Outlook Add-In written using C# and VSTO, I am looking to capture the text of the latest Reply email to a thread.
The problem is that all the properties on a MailItem object such as Body, HTMLBody etc give the entire text of the email including past replies. I am looking to somehow only get the most recent text. And I need to be able to do this considering multiple languages in an email.
Here is what i have tried
Using Bookmarks on the MailEditor - There seems to be no more _MailOriginal bookmark with exchange and Outlook
Somehow trying to get hold off MIME properties - I don't know enough on which properties to pick and how to use them to parse the recent text.
You cannot do that even in theory: imagine a user typing at the top of the message (e.g. "see below") and then inserting/deleting various pieces in the message body below (I do that all the time). You are lucky if the font color is different.
You can try to compare the original with the new and figure out the diff, but that requires access to the original message. You can look at the PR_IN_REPLY_TO_ID MAPI property (DASL name http://schemas.microsoft.com/mapi/proptag/0x1042001F) and try to find the original message either in the Inbox or the Sent Items folder. Note that in the latter case (Sent Items folder) the property might not be available on the cached message, you'd need to search the online version of the folder (cannot do that in OOM, you'd need Extended MAPI in C++ or Delphi or Redemption in any language).
The Outlook object model doesn't provide anything for that. You need to parse the message body string on your own.
Also, you can iterate over all items in the conversation and detect each of them in the latest/recent item. By removing older items you can get the latest. The following example shows how to get and display mail items in a conversation.
void DemoConversation()
{
object selectedItem = Application.ActiveExplorer().Selection[1];
// For this example, you will work only with
//MailItem. Other item types such as
//MeetingItem and PostItem can participate
//in Conversation.
if (selectedItem is Outlook.MailItem)
{
// Cast selectedItem to MailItem.
Outlook.MailItem mailItem = selectedItem as Outlook.MailItem;
// Determine store of mailItem.
Outlook.Folder folder = mailItem.Parent as Outlook.Folder;
Outlook.Store store = folder.Store;
if (store.IsConversationEnabled == true)
{
// Obtain a Conversation object.
Outlook.Conversation conv = mailItem.GetConversation();
// Check for null Conversation.
if (conv != null)
{
// Obtain Table that contains rows
// for each item in Conversation.
Outlook.Table table = conv.GetTable();
Debug.WriteLine("Conversation Items Count: " + table.GetRowCount().ToString());
Debug.WriteLine("Conversation Items from Table:");
while (!table.EndOfTable)
{
Outlook.Row nextRow = table.GetNextRow();
Debug.WriteLine(nextRow["Subject"]
+ " Modified: "
+ nextRow["LastModificationTime"]);
}
Debug.WriteLine("Conversation Items from Root:");
// Obtain root items and enumerate Conversation.
Outlook.SimpleItems simpleItems = conv.GetRootItems();
foreach (object item in simpleItems)
{
// In this example, enumerate only MailItem type.
// Other types such as PostItem or MeetingItem
// can appear in Conversation.
if (item is Outlook.MailItem)
{
Outlook.MailItem mail = item as Outlook.MailItem;
Outlook.Folder inFolder = mail.Parent as Outlook.Folder;
string msg = mail.Subject
+ " in folder " + inFolder.Name;
Debug.WriteLine(msg);
}
// Call EnumerateConversation
// to access child nodes of root items.
EnumerateConversation(item, conv);
}
}
}
}
}
void EnumerateConversation(object item, Outlook.Conversation conversation)
{
Outlook.SimpleItems items = conversation.GetChildren(item);
if (items.Count > 0)
{
foreach (object myItem in items)
{
// In this example, enumerate only MailItem type.
// Other types such as PostItem or MeetingItem
// can appear in Conversation.
if (myItem is Outlook.MailItem)
{
Outlook.MailItem mailItem = myItem as Outlook.MailItem;
Outlook.Folder inFolder = mailItem.Parent as Outlook.Folder;
string msg = mailItem.Subject
+ " in folder " + inFolder.Name;
Debug.WriteLine(msg);
}
// Continue recursion.
EnumerateConversation(myItem, conversation);
}
}
}
In the sample code example, we get a selected MailItem object and then determine the store of the MailItem object by using the Store property of the Folder object. DemoConversation then checks whether the IsConversationEnabled property is true; if it is true, the code example gets Conversation object by using the GetConversation method. If the Conversation object is not a null reference, the example gets the associated Table object that contains each item in the conversation by using the GetTable method. The example then enumerates each item in the Table and calls EnumerateConversation on each item to access the child nodes of each item. EnumerateConversation takes a Conversation object and gets the child nodes by using the GetChildren(Object) method. EnumerateConversation is called recursively until there are no more child nodes. Each conversation item is then displayed to the user.
The following code is ment to create an e-mail and attach the one or multiple attachemnts. So far the code does just that with success, but it also creates the equal amount of e-mails with the right amount of attachments in each of them. I.e. with three attachments it creates three e-mails all with the three attachments in each of them.
private void SendMail(List<string> paths)
{
DateTime defDt = DateTime.Now.AddMinutes(3);
Microsoft.Office.Interop.Outlook.Application app = new Microsoft.Office.Interop.Outlook.Application();
Microsoft.Office.Interop.Outlook.MailItem mailItem = app.CreateItem(Microsoft.Office.Interop.Outlook.OlItemType.olMailItem);
mailItem.BodyFormat = Microsoft.Office.Interop.Outlook.OlBodyFormat.olFormatHTML;
mailItem.GetInspector.Activate();
var signature = mailItem.HTMLBody;
mailItem.HTMLBody = signature;
foreach (string u in paths)
{
mailItem.Attachments.Add(u);
}
mailItem.DeferredDeliveryTime = defDt;
mailItem.Display(mailItem);
}
Why does it behave like this? There are no foreach loop besides the one for adding the multiple attachments. When firing with one attachment the error doesn't show (probably since it i creating just one e-mail with one attachment as excpected). Any thoughts from this great community?
The code was called in a foreach loop earlier in the code. The code presented works as expected.
Hello I recently got into development around EWS. One of the issue came up to me is that a client ask me to import emails into database and he wants to detect the duplicate based on InternetMessageID this way he doesn't have to import the duplicate emails and my code came up to this point.
private static string GetInternetMessageID(Microsoft.Exchange.WebServices.Data.Item email)
{
EmailMessage emailMsg = email as EmailMessage;
string returnId = string.Empty;
if ((emailMsg != null)) {
try {
emailMsg.Load();
//loads additional info, without calling this ToRecipients (and more) is empty
} catch (ArgumentException ex) {
//retry
email.Load();
}
returnId = emailMsg.InternetMessageId;
} else {
//what to do?
}
return returnId;
}
I can handle regular emails, but for special exchange objects such as contact, Calendar, Posts etc it does not work because it could not cast it to an EmailMessage object.
And I know you can extract the internetMessageId from those objects. Because the client used to have another software that extract this ID for them, maybe the property is not called internetMessageID, I think I probally have to extract it from the internetMessageHeader. However when ever I try to get it from the item object it just throws me an error. How do I get the internet messageID from these "Special" exchange items?
PS i am aware of item.id.UniqueID however that is not what I want as this id changes if I move items from folder to another folder in exchange
Only objects that have been sent via the Transport service will have an InternetMessageId so things like Contacts and Tasks because they aren't messages and have never been routed via the Transport service will never have an Internet MessageId. You probably want to look at using a few properties to do this InternetMessageId can be useful for messages PidTagSearchKey https://msdn.microsoft.com/en-us/library/office/cc815908.aspx is one that can be used (if you good this there are various examples of using this property).
If your going to use it in Code don't use the method your using to load the property on each item this is very inefficient as it will make a separate call for each object. Because these I'd's are under 256 Kb just retrieve then when using FindItems. eg
ExtendedPropertyDefinition PidTagSearchKey = new ExtendedPropertyDefinition(0x300B, MapiPropertyType.Binary);
ExtendedPropertyDefinition PidTagInternetMessageId = new ExtendedPropertyDefinition(0x1035, MapiPropertyType.String);
PropertySet psPropSet = new PropertySet(BasePropertySet.IdOnly);
psPropSet.Add(PidTagSearchKey);
psPropSet.Add(PidTagInternetMessageId);
ItemView ItemVeiwSet = new ItemView(1000);
ItemVeiwSet.PropertySet = psPropSet;
FindItemsResults<Item> fiRess = null;
do
{
fiRess = service.FindItems(WellKnownFolderName.Inbox, ItemVeiwSet);
foreach (Item itItem in fiRess)
{
Object SearchKeyVal = null;
if (itItem.TryGetProperty(PidTagSearchKey, out SearchKeyVal))
{
Console.WriteLine(BitConverter.ToString((Byte[])SearchKeyVal));
}
Object InternetMessageIdVal = null;
if (itItem.TryGetProperty(PidTagInternetMessageId, out InternetMessageIdVal))
{
Console.WriteLine(InternetMessageIdVal);
}
}
ItemVeiwSet.Offset += fiRess.Items.Count;
} while (fiRess.MoreAvailable);
If you need larger properties like the Body using the LoadPropertiesForItems Method https://blogs.msdn.microsoft.com/exchangedev/2010/03/16/loading-properties-for-multiple-items-with-one-call-to-exchange-web-services/
I'm trying to use the attachments included in calendar items pulled progmatically.
I have a list of chosen calendar subject lines from a previous dialog box, and while the subject is transferring properly, the body isn't working well (another question altogether) but the attachments aren't working whatsoever.
Here's my foreach loop where the attachments are being placed into an Attachments array for use later:
string[] subjects = new string[dialog.chosen.Count];
string[] bodies = new string[dialog.chosen.Count];
Attachments[] attach = new Attachments[dialog.chosen.Count];
foreach (Outlook.AppointmentItem appt in rangeAppts)
{
foreach (string text in dialog.chosen)
{
if (text == appt.Subject)
{
subjects[i] = appt.Subject;
bodies[i] = Convert.ToString(appt.Body);
attach[i] = appt.Attachments;
i = i + 1;
}
}
}
And then here's where I actually call the method:
sendEmailTemplate(bodies[i], subject, to, "", attachment: attach[i]);
And then the method itself:
public void sendEmailTemplate(string body, string subject, string to, string cc , Attachments attachment = null)
{
Microsoft.Office.Interop.Outlook.Application oApp = new Microsoft.Office.Interop.Outlook.Application();
Microsoft.Office.Interop.Outlook._MailItem oMailItem = (Microsoft.Office.Interop.Outlook._MailItem)oApp.CreateItem(Microsoft.Office.Interop.Outlook.OlItemType.olMailItem);
oMailItem.HTMLBody = body;
oMailItem.Subject = subject;
try
{
oMailItem.Attachments.Add(attachment);
}
catch {}
oMailItem.To = to;
oMailItem.CC = cc;
oMailItem.Display(false);
oMailItem.Application.ActiveInspector().WindowState = Microsoft.Office.Interop.Outlook.OlWindowState.olNormalWindow;
}
I've tried several things, however when I actually go to send the e-mail, I end up getting:
Exception: Member not found. HRESULT: 0x80020003
And then I haven't been able to get anything else to work. The try/catch loop on the method is to prevent the above exception as I was getting that exception regardless of whether or not an attachment was present, and now attachments just aren't being added.
I'm using Interop that comes with Office along with C#. Winforms if that makes a difference.
MailItem.Attachments takes either a string (fully qualified file name), or another Outlook item (MailItem, ContactItem, etc.).
You cannot pass Attachments object as an argument. If you need to copy the attachments, loop through all attachments in the Attachments collection, call Attachment.SaveAsFile for each attachment, pass the file name to MailItem.Attachments.Add, delete thee temporary file.
I'm new to office addins. I'm an MVC programmer but this project has been dumped on me as no one else wants to do it. I need to create an outlook addin that will forward all email data to a service where communications can be tracked by a recruitment system.
I am using
Application.ItemSend += new Microsoft.Office.Interop.Outlook.ApplicationEvents_11_ItemSendEventHandler(saveEmail);
where I then cast the email into a Outlook.MailItem. The problem is I see no way of getting the from and to email addresses. All it gives me is the name of the people. Am I missing something?
So far the best solution I can think of is to save the msg as a .msg file. Forward that to my service and then user a parser I found to convert it to HTML.
Any suggestions?
To access the recipients, loop through MailItem.Recipients collection and access Recipient.Name and Recipient.Address properties.
Sender related properties are not yet set by the time ItemSend event fires - the earliest you can access sender properties is when Items.ItemAdd event fires on the Sent Items folder (retrieve it using Namespace.GetDefaultFolder).
You can read the MailItem.SendUsingAccount. If it is null, use the first Account from the Namespace.Acounts collection. You can then use Account.Recipient object.
Keep in mind that you should not blindly cast outgoing items to MailItem objects - you can also have MeetingItem and TaskRequestItem objects.
OK using the info given to me by Dmitry Streblechenko and some other info I just looked up here is my solution so far.
In the ItemSend event I first make sure that sent email is moved to the default sent items folder. I'm testing outlook using gmail so normally these will go elsewhere. sentMailItems is made as a class field as apparently it will get garbage collected if its just declared inside the Startup function (Something quite odd to me an MVC programmer :) ).
I' will test this on exchange when I get back to office an hopefully all goes well.
public partial class ThisAddIn
{
public Outlook.Items sentMailItems;
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
Application.ItemSend += new Microsoft.Office.Interop.Outlook.ApplicationEvents_11_ItemSendEventHandler(ItemSend);
sentMailItems = Application.Session.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderSentMail).Items;
sentMailItems.ItemAdd += new Outlook.ItemsEvents_ItemAddEventHandler(Items_ItemAdd);
}
void Items_ItemAdd(object item)
{
MessageBox.Show(((Outlook.MailItem)item).Subject);
var msg = Item as Outlook.MailItem;
string from = msg.SenderEmailAddress;
string allRecip = "";
foreach (Outlook.Recipient recip in msg.Recipients)
{
allRecip += "," + recip.Address;
}
}
private void ItemSend(object Item, ref bool Cancel)
{
if (!(Item is Outlook.MailItem))
return;
var msg = Item as Outlook.MailItem;
msg.DeleteAfterSubmit = false; // force storage to sent items folder (ignore user options)
Outlook.Folder sentFolder = this.Application.Session.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderSentMail) as Outlook.Folder;
if (sentFolder != null)
msg.SaveSentMessageFolder = sentFolder; // override the default sent items location
msg.Save();
}
//Other auto gen code here....
}