I am working on a software where i am able to retrieve the inbox and sent items from outlook. What i want to do is to relate the inbox emails with replies (if someone has sent a reply to that email). The list should be displayed in this order
Sender#abc.com Incoming Subject Received Time
sender#abc.com Reply Subject Sent time
What i am planning to do is to retrieve inbox items in one datatable and the sent items in another datatable. It reads the emails one by one on the basis of sender email and the subject, then searches that sender and email in the sent items and if it matches, merge that to a third datatable.
is there any other better way to do so?
Here is the code:
private DataTable GetInboxItems()
{
DataTable inboxTable;
//try
//{
filter = "[ReceivedTime] >= '" + dtpStartDate.Value.ToString("dd/MM/yyyy 12:00 AM") + "' and [ReceivedTime] <= '" + dtpEndDate.Value.ToString("dd/MM/yyyy 11:59 PM") + "'";
Outlook.Application outlookApp = GetApplicationObject();
Outlook.Folder root = outlookApp.Session.DefaultStore.GetRootFolder() as Outlook.Folder;
EnumerateFolders(root);
//string filter = "[ReceivedTime] > '" + dtpStartDate.Value.ToString("dd/MM/yyyy") + "'";
//inbox
Outlook.MAPIFolder inboxFolder = outlookApp.Session.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
inboxTable = CreateTable();
int count = 0;
if (inboxFolder.Items.Count > 0)
{
var restrictedItems = inboxFolder.Items.Restrict(filter);
restrictedItems.Sort("[ReceivedTime]", true); //descending
//foreach (var item in inboxFolder.Items)
foreach (var item in restrictedItems)
{
var mail = item as Outlook.MailItem;
if (mail != null)
{
//try
//{
DataRow row = inboxTable.NewRow();
//row["sn"] = (++count).ToString();
row["sn"] = mail.EntryID + " " + mail.ReceivedByEntryID;
row["MailType"] = "Inbox";
row["SenderName"] = mail.SenderName;
row["SenderEmail"] = mail.SenderEmailAddress;
row["ReceivedDate"] = mail.ReceivedTime;
row["Subject"] = mail.Subject;
row["Body"] = mail.Body != null ? (mail.Body.Length > 25 ? mail.Body.Substring(0, 25) : mail.Body) : null;
//row["Body"] = mail.Body != null ? mail.Body : "";
row["MailSize"] = mail.Size.ToString();
string attachments = null;
if (mail.Attachments.Count > 0)
{
foreach (var attachment in mail.Attachments)
{
if (((Outlook.Attachment)attachment) != null)
//attachments = ((Outlook.Attachment)attachment).FileName + " " + ((Outlook.Attachment)attachment).Size.ToString() + ", ";
attachments += (((Outlook.Attachment)attachment).Size / 1024).ToString() + " KB, ";
}
}
row["AttachmentCount"] = mail.Attachments.Count;
if (attachments != null)
row["AttachmentSize"] = attachments.Substring(0, attachments.Length - 2);
inboxTable.Rows.Add(row);
}
//catch (Exception ex)
//{
// return null;
//}
}
}
return inboxTable;
}
I've made this kind of stuff in an outlook add-in
There is no 100% way to make it ...
The problem is that the conversation ID is not always kept by other software. So you need to use a set of data to link email to their answers:
- Message-ID: this is available in the email headers. Sent items doesn't have this :/
- In-Reply-To: this is also in the email headers
- Topic ID
For the topic ID, I retrieve values in this order (I take the first available):
- MailItem.ConversationIndex: Each reply add bytes to the conversation index
- Reference header
Then, I link email to their reply using topicID, email have the same first X characters than reply. Example original email topic id = abc, reply = abcdef
For all mails than cannot be linked using conversation id, I try to link using Message-ID & In-Reply-To ID
Problem will particularly comes from email sent by outlook (no Message-ID) then user reply without Reference/ConversationIndex header... you'll not have any way to link both mail together.
Hope it helps
EDIT: Here's some code. I've copy/pasted code from different class/method to create a single method, so it may not compile. Take it more as a pseudo-code.
public SimpleTree<MailData> CreateTree(List<MailData> mails)
{
mails.Sort((m1, m2) => m1.TopicId == m2.TopicId ? m2.CreationDate.CompareTo(m1.CreationDate) : m1.TopicId.CompareTo(m2.TopicId));
var tree = new SimpleTree<MailData>();
var i = 0;
while (i < mails.Count)
{
var node = tree.Children.Add(mails[i]);
var topicId = mails[i].TopicId;
var start = i + 1;
while (start < mails.Count
&& !string.IsNullOrEmpty(topicId)
&& !string.IsNullOrEmpty(mails[start].TopicId)
&& mails[start].TopicId.StartsWith(topicId))
{
node.Children.Add(mails[start]);
start++;
}
i = start;
}
// Handle email where TopicId are different, but ParentId is filled with correct value
for (int j = tree.Children.Count - 1; j >= 0; j--)
{
var child = tree.Children[j];
if (child.Children.Count == 0 && !string.IsNullOrEmpty(child.Value.ParentId))
{
var parentNode = tree.FindNode(s => s != null && s.MessageId == child.Value.ParentId);
if (parentNode != null && parentNode != child)
parentNode.Children.Add(child);
}
}
return tree;
}
MailData is a class with the 3 fields needed as explained before:
MessageID (from Message-ID header)
ParentId (from In-Reply-To header)
TopicId (from ConversationIndex or Reference header)
SimpleTree<> is a class to create tree, it's in fact a node with children. Nothing special or related to email here. The .Value property refer to the data associated to the node (MailData here)
The goal is to sort on the TopicId so that we can construct the tree in 1 loop
Then I check all the mail in the tree root to check if we can move them under another mail usine MessageId/ParentId
Just remember that it create a one-level tree, something like:
Mail A
Reply AA
Reply AAA
Mail B
Reply BB
Reply BBB
But you would need something like this:
Mail A
Reply AA
Reply AAA
Mail B
Reply BB
Reply BBB
Related
I'm a complete newbie to EWS, but am trying to convert a happily functioning IMAP program into EWS & am having problems accessing simple fields in the managed API, e.g. From, Sender, BodyType. Can anyone spot what I am doing wrong? Many thanks folks.
ItemView view = new ItemView(99);
SearchFilter.Exists filter = new SearchFilter.Exists(EmailMessageSchema.Id);
FindItemsResults<Item> inboxMessageList = service.FindItems(WellKnownFolderName.Inbox, view);
Console.WriteLine("Inbox message count: " + inboxMessageList.TotalCount);
int messageCounter = 1;
//message loop
foreach (Item thisMessage in inboxMessageList)
{
//Collect info about current email message
Item thisItem = Item.Bind(service, thisMessage.Id);
Console.WriteLine("Current message ID: " + thisMessage.Id);
string uniqueID = "EMAIL-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmss-fff");
string messageTo = thisItem.DisplayTo;
string messageCC = thisItem.DisplayCc;
string messageFrom = //cant get this to work
string messageSubject = thisItem.Subject;
string messageDate = thisMessage.DateTimeReceived.ToString();
int noOfAttachments = 0;
Boolean messageHasAttachments = thisMessage.HasAttachments;
if (messageHasAttachments) noOfAttachments = thisMessage.Attachments.Count();
string isBodyHtml = //cant seem to implement this either
Boolean domainblacklistResult = fn.CheckIfDomainBlacklisted(messageFrom);
Boolean emailblacklistResult = fn.CheckIfEmailBlacklisted(messageFrom);
To access information about the email message, you need to bind it as an EmailMessage, instead of as an Item. Example:
EmailMessage message = EmailMessage.Bind(service, thisMessage.Id);
I have following requirement :
Allow the user to drag & drop an email from outlook to a datagrid
Prepare a reply mail, and show it so the user can review and send
After sending, also fetch the send mail and put it into the datagrid
The drag/drop I have working
Preparing the replay email and showing it to the user I also have working, with this code :
MailItem mail = GetMailBySubject(dateReceived, subject);
if (mail != null)
{
MailItem mailReply = mail.ReplyAll();
// add text and stuff to mailReply...
mailReply.Display();
}
This will open a window in outlook, as if the user clicked reply in outlook.
Now I am stuck with the 3th requirement,
after the user send the reply email, I need somehow to find this email in outlook to add it to my datagrid.
But I have no clue on how to do that.
All I have is the original mail that is been used to prepare the reply.
Is there a way to find the reply with only this, or is this maybe a complete wrong approach ?
To make it more difficult is that I have to show the reply email NON Modal, so I have no trigger when the user clicked on send in outlook.
for reference, here is the code for GetMailBySubject
private MailItem GetMailBySubject(DateTime dateReceived, string subject)
{
MailItem Result = null;
Microsoft.Office.Interop.Outlook.Application OutlookIns = new Microsoft.Office.Interop.Outlook.Application();
Microsoft.Office.Interop.Outlook.NameSpace olNamespace = OutlookIns.GetNamespace("MAPI");
MAPIFolder myInbox = olNamespace.GetDefaultFolder(OlDefaultFolders.olFolderInbox);
Items items = myInbox.Items;
int count = items.Count;
MailItem mail = null;
int i = 1; //DO NOT START ON 0
while ((i < count) && (Result == null))
{
if (items[i] is MailItem)
{
mail = (MailItem)items[i];
if ((mail.ReceivedTime.ToString("yyyyMMdd hh:mm:ss") == dateReceived.ToString("yyyyMMdd hh:mm:ss")) && (mail.Subject == subject))
{
Result = mail;
}
}
i++;
}
return Result;
}
EDIT
I tried this code as suggested, but the Items.ItemAdd event is not firing.
So I must still be doing something wrong but I cant see it
this is my code now
MailItem mail = GetMailBySubject((DateTime)sentOn, msg.Subject);
if (mail != null)
{
MailItem mailReply = mail.ReplyAll();
mailReply.HTMLBody = GetDefaultReplyText() + Environment.NewLine + mailReply.HTMLBody;
Guid guid = Guid.NewGuid();
UserProperties mailUserProperties = null;
UserProperty mailUserProperty = null;
mailUserProperties = mailReply.UserProperties;
mailUserProperty = mailUserProperties.Add("myproperty", OlUserPropertyType.olText);
mailUserProperty.Value = guid.ToString(); ;
// the code below gives error "The property cannot be parsed or has an invalid format"
//mailReply.PropertyAccessor.SetProperty("myproperty", guid.ToString());
mailReply.Display();
}
private MailItem GetMailBySubject(DateTime dateReceived, string subject)
{
MailItem Result = null;
Microsoft.Office.Interop.Outlook.Application OutlookIns = new Microsoft.Office.Interop.Outlook.Application();
Microsoft.Office.Interop.Outlook.NameSpace olNamespace = OutlookIns.GetNamespace("MAPI");
MAPIFolder myInbox = olNamespace.GetDefaultFolder(OlDefaultFolders.olFolderInbox);
Items items = myInbox.Items;
int count = items.Count;
MailItem mail = null;
int i = 1; //DO NOT START ON 0
while ((i < count) && (Result == null))
{
if (items[i] is MailItem)
{
mail = (MailItem)items[i];
if ((mail.ReceivedTime.ToString("yyyyMMdd hh:mm:ss") == dateReceived.ToString("yyyyMMdd hh:mm:ss")) && (mail.Subject == subject))
{
Result = mail;
MAPIFolder mySent = olNamespace.GetDefaultFolder(OlDefaultFolders.olFolderSentMail);
mySent.Items.ItemAdd += Items_SentMailItemAdd;
}
}
i++;
}
return Result;
}
and finally
private void Items_SentMailItemAdd(object Item)
{
//throw new NotImplementedException();
; // this event is never fired
}
You can use the Items.ItemAdd event on the Sent Items folder. To check if that is your message, set a custom property on the message that you create and display. You can use MailItem.UserProperties.Add, but that can force the message to be sent in the TNEF format. To prevent that from happening, you can use MailItem.PropertyAccessro.SetProperty to set a named MAPI property without using the UserProperties collection. You can set a test user property and look at its DASL name (to be used by SetProperty) with OutlookSpy (I am its author - select the message, click IMessage button, select your custom property, see the DASL edit box).
foreach (var part in emailInfoResponse.Result.Payload.Parts)
{
if (part.Parts != null)
foreach (var innerPart in part.Parts)
{
if (innerPart.MimeType == "text/plain")
{
body = innerPart.Body.Data;
}
}
}
I am successfully reading the body ONLY of the main mail. It is not taking the data from the reply messages assigned to the mail,
any ideas how I can I read the replies to the mail too? Or even if there is a way to take only the replies of a specific mail
To get later / other messages in a conversation, you need to retrieve the source thread:
String threadID = emailInfoResponse.Result.ThreadId;
Thread thread = service.Users.Threads.Get(userID, threadID).Execute();
foreach( var msg in thread.Messages )
{
/* your code for each message */
}
Per the Gmail message reference, messages can share a threadId if they have both the correct headers and the same subject. Otherwise, they will be on different email threads.
You'd probably want to refactor a bit and start from a ThreadList. This way, you can find all conversations that match a specific input query (to find only the messages that correspond to these client lists):
Make data storage object: Dictionary<String, Container>, where the string is the company (from the email subject, or wherever), and the container probably only needs unique values so HashSet or similar.
Page through the ThreadList
For each thread
Page through messages
For each message, if the first message, get the subject i.e. company name (all messages have to share a subject in the same thread).
Then append the clients to the data storage object based on the subject (company name).
Construct your database records from the data storage object as appropriate.
public static MimeKit.MimeMessage Reply(MimeKit.MimeMessage message, bool replyToAll)
{
var reply = new MimeKit.MimeMessage();
if (message.ReplyTo.Count > 0)
{
reply.To.AddRange(message.ReplyTo);
}
else if (message.From.Count > 0)
{
reply.To.AddRange(message.From);
}
else if (message.Sender != null)
{
reply.To.Add(message.Sender);
}
if (replyToAll)
{
reply.To.AddRange(message.To);
reply.Cc.AddRange(message.Cc);
}
if (!message.Subject.StartsWith("Re:", StringComparison.OrdinalIgnoreCase))
reply.Subject = "Re:" + message.Subject;
else
reply.Subject = message.Subject;
if (!string.IsNullOrEmpty(message.MessageId))
{
reply.InReplyTo = message.MessageId;
foreach (var id in message.References)
reply.References.Add(id);
reply.References.Add(message.MessageId);
}
using (var quoted = new StringWriter())
{
var sender = message.Sender ?? message.From.Mailboxes.FirstOrDefault();
quoted.WriteLine("On {0}, {1} wrote:", message.Date.ToString("f"), !string.IsNullOrEmpty(sender.Name) ? sender.Name : sender.Address);
using (var reader = new StringReader(message.TextBody))
{
string line;
while ((line = reader.ReadLine()) != null)
{
quoted.Write("> ");
quoted.WriteLine(line);
}
}
reply.Body = new MimeKit.TextPart("plain")
{
Text = quoted.ToString()
};
}
return reply;
}
I am working on addin to Outlook 2013. I added custom field to MailItem and used AdvancedSearch to find item. The last missing part is showing the results.
How can I show results of custom search in search results?
private void Application_AdvancedSearchComplete(Outlook.Search SearchObject)
{
string dx = SearchObject.Tag;
int x = SearchObject.Results.Count;
//What next?
}
private void button1_Click(object sender, RibbonControlEventArgs e)
{
Object selObject = Globals.ThisAddIn.Application.ActiveExplorer().Selection[1];
Outlook.MailItem mail = selObject as Outlook.MailItem;
if (mail != null)
{
Outlook.UserProperties mailUserProperties = null;
Outlook.UserProperty mailUserProperty = null;
mailUserProperties = mail.UserProperties;
foreach (var i in mailUserProperties)
{
var xx = i;
}
mailUserProperty = mailUserProperties.Add("TestUserProperty", Outlook.OlUserPropertyType.olText, true);
mailUserProperty.Value = "Eugene Astafiev";
mail.Save();
}
string str = "http://schemas.microsoft.com/mapi/string/{00020329-0000-0000-C000-000000000046}/TestUserProperty LIKE '%ugene%'";
Outlook.MAPIFolder inbox = Globals.ThisAddIn.Application.Session.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
Globals.ThisAddIn.Application.AdvancedSearch("Inbox", str, false, "TestUserProperty");
}
You cannot display the results of Application.AdvancedSearch. To show search results in the UI you will have to use Explorer.Search, which is effectively just automating the user performing the search in the UI. However, you will not get the results back in code to work with. See here for an overview of your options:
https://msdn.microsoft.com/en-us/library/ff869846.aspx
You can save results to a search folder and then display it to a user. The Save method of the Search class saves the search results to a Search Folder. Note, the Save method displays an error if a Search Folder with the same name already exists.
Outlook.Results advancedSearchResults = advancedSearch.Results;
if (advancedSearchResults.Count > 0)
{
if (HostMajorVersion > 10)
{
object folder = advancedSearch.GetType().InvokeMember("Save",
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.InvokeMethod |
System.Reflection.BindingFlags.Public,
null, advancedSearch,
new object[] { advancedSearchTag });
}
else
{
strBuilder = new System.Text.StringBuilder();
strBuilder.AppendLine("Number of items found: " +
advancedSearchResults.Count.ToString());
for (int i = 1; i < = advancedSearchResults.Count; i++)
{
resultItem = advancedSearchResults[i]
as Outlook.MailItem;
if (resultItem != null)
{
strBuilder.Append("#" + i.ToString());
strBuilder.Append(" Subject: " + resultItem.Subject);
strBuilder.Append(" \t To: " + resultItem.To);
strBuilder.AppendLine(" \t Importance: " +
resultItem.Importance.ToString());
Marshal.ReleaseComObject(resultItem);
}
}
if (strBuilder.Length > 0)
System.Diagnostics.Debug.WriteLine(strBuilder.ToString());
else
System.Diagnostics.Debug.WriteLine(
"There are no Mail items found.");
}
}
else
{
System.Diagnostics.Debug.WriteLine("There are no items found.");
}
You may find the Advanced search in Outlook programmatically: C#, VB.NET article helpful.
I am using EWS to create a StreamingSubscription on an inbox. It is listening for the NewMail event. I am able to pull the From Address, Subject, Body, To Address, CC Address but not the BCC Address. Is there any way to see this list?
CODE:
static void OnEvent(object sender, NotificationEventArgs args)
{
String from = null;
String subject = null;
String body = null;
String to = null;
StreamingSubscription subscription = args.Subscription;
// Loop Through All Item-Related Events
foreach (NotificationEvent notification in args.Events)
{
ItemEvent item = (ItemEvent)notification;
PropertySet propertySet = new PropertySet(ItemSchema.UniqueBody);
propertySet.RequestedBodyType = BodyType.Text;
propertySet.BasePropertySet = BasePropertySet.FirstClassProperties;
// Parse Email
EmailMessage message = EmailMessage.Bind(service, item.ItemId, propertySet);
from = message.From.Address;
subject = message.Subject;
body = message.Body.Text;
if (message.ToRecipients.Count > 0)
{
to = message.ToRecipients[0].Address;
body += "\n TO FIELD";
}
else if (message.CcRecipients.Count > 0)
{
to = message.CcRecipients[0].Address;
body += "\n CC FIELD";
}
/************** Does not work! BccRecipients is always empty *****************/
else if (message.BccRecipients.Count > 0)
{
to = message.BccRecipients[0].Address;
body += "\n BCC FIELD";
}
/************* REST OF CODE ************************/
}
}
That would kind of defeat the point of a blind-carbon-copy. I dont believe it can be done.
Consider using the Journaling feature of Exchange. This uses something called "Envelope Journaling" which includes BCC information for messages within the Exchange environment.
For everything that comes from external sources (gmail) no BCC information is available.
This might help:
http://gsexdev.blogspot.com/2011/06/processing-bccs-in-exchange-transport.html