I want to automate Outlook so that I can download the "pieces parts" of email messages so that I can tie related messages together. I understand that email usually has a "MessageID" to serve this purpose, so that emails can be viewed in context, as "threads" in a newsreader are tied together.
Does Outlook have the notion of "Message IDs" in emails sent with it? I see that the elements that can be extracted (using automation) are Subject, SenderEmail, CreationTime, Body, SenderName, and HTMLBody. Is a "message id" or equivalent available somewhere, too?
Outlook tracks related messages by using Conversations.
In Outlook 2003, there is ConversationTopic (MAPI: PR_CONVERSATION_TOPIC) & ConversationIndex
(MAPI: PR_CONVERSATION_INDEX). ConversationTopic is typically the message subject (minus prefixes - RE:/FW:, etc.), while ConversationIndex represents the sequential ordering of the ConversationTopic (essentially GUID + timestamp). See Working with Conversations on MSDN. ConversationIndex is explicitly defined on MSDN here.
In Outlook 2010, they added ConversationID (MAPI: PR_CONVERSATION_ID) which is derived from the ConversationTopic. ConversationID can be generated from the ConversationTopic as discussed here.
For more detailed info about the MSG protocol specs regarding Conversations see [MS-OXOMSG]: E-Mail Object Protocol Specification, section 2.2.1.2 and 2.2.1.3.
Small addition to previous great answer. In case if anyone else will also need C# implementation of algorithm used to retrieve ConversationID from ConversationIndex/ConversationTopic:
private const int c_ulConvIndexIDOffset = 6;
private const int c_ulConvIndexIDLength = 16;
private string GetConversationId()
{
var convTracking = GetMapiPropertyBool(PR_CONVERSATION_INDEX_TRACKING);
var convIndex = GetMapiPropertyBytes(PR_CONVERSATION_INDEX);
byte[] idBytes;
if (convTracking
&& convIndex != null
&& convIndex.Length > 0)
{
// get Id from Conversation index
idBytes = new byte[c_ulConvIndexIDLength];
Array.Copy(convIndex, c_ulConvIndexIDOffset, idBytes, 0, c_ulConvIndexIDLength);
}
else
{
// get Id from Conversation topic
var topic = GetMapiPropertyString(PR_CONVERSATION_TOPIC);
if (string.IsNullOrEmpty(topic))
{
return string.Empty;
}
if (topic.Length >= 265)
{
topic = topic.Substring(0, 256);
}
topic = topic.ToUpper();
using (var md5 = new System.Security.Cryptography.MD5CryptoServiceProvider())
{
idBytes = md5.ComputeHash(Encoding.Unicode.GetBytes(topic));
}
}
return BitConverter.ToString(idBytes).Replace("-", string.Empty);
}
GetMapiProperty...() is a helper functions which just retrieve required MAPI property and cast result to appropriate managed type
Related
I am getting emails from a mailbox using exchange webservices using a custom code block in C# (I'm not versed in C# at all so forgive code quality!) in UiPath. I am passing the exchange service and folder ID as arguments to the code block. I noticed that when there was a large attachment on the email it took significantly longer. I am not interested in the attachment I just want to be able to access some information about the email. This was my initial code:
//Search for oldest email
ItemView objView = new ItemView(1);
objView.OrderBy.Add(ItemSchema.DateTimeReceived, SortDirection.Ascending);
FindItemsResults<Item> lstItems = objServer.FindItems(in_FolderID, objView);
//Bind to email from result
if(lstItems.Count() == 0)
{
Console.WriteLine("Inbox appears to be empty");
out_ExchangeMessage = null;
out_InternetMessageID = null;
}
else
{
Item objItem = lstItems.ElementAt(0);
Console.WriteLine("Retrieving email: " + objItem.Subject);
PropertySet objPropertySet = new PropertySet(BasePropertySet.FirstClassProperties, ItemSchema.MimeContent, EmailMessageSchema.IsRead,ItemSchema.Attachments,ItemSchema.TextBody);
out_ExchangeMessage = EmailMessage.Bind(objServer,objItem.Id, objPropertySet);
out_InternetMessageID = out_ExchangeMessage.InternetMessageId;
Console.WriteLine("Message Retrieved: " + out_ExchangeMessage.InternetMessageId);
}
I tried removing ItemSchema.Attachments so this line reads as follows. But the email still takes significantly longer to download
PropertySet objPropertySet = new PropertySet(BasePropertySet.FirstClassProperties, ItemSchema.MimeContent, EmailMessageSchema.IsRead,ItemSchema.TextBody);
Is there a way to speed up the retrieving of emails with large attachments?
Because your including ItemSchema.MimeContent in your propertyset that is going to give you the whole MimeStream of the Message including attachments. If you don't require the MimeStream don't request it. Eg you should be able to get all the other properties of the Message eg body,subject,headers etc from other properties so it would only be required if you wanted to save the message.
I am using Azure Information Protection Unified Labeling client to label emails. We are still using PGP in our environment and emails classified strictly confidential must be PGP encrypted.
When the email is sent, I try to find out, how the email is classified and trigger PGP encryption, when the classification is strictly confidential. This is done in an Outlook VSTO c# Add-in.
To find out the classification, I read the email header property "msip_labels" which is set by AIP and contains all necessary information. I am using the following procedure to read the headers. The code is far away from being perfect. I am just figuring out, how to get the value.:
private void GetHeaders()
{
var mail = (Outlook.MailItem)Application.ActiveInspector().CurrentItem;
var propertyAccessor = mail.PropertyAccessor;
try
{
var custom = propertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/string/{00020386-0000-0000-C000-000000000046}/msip_labels");
}
catch(Exception ex)
{
var message = ex.Message;
}
}
I am able to read properties, set by another tool, but the AIP property is multiline. When the code is executed, I get the Error: Typeconflict. (Exception of HRESULT: 0x80020005 (DISP_E_TYPEMISMATCH))
Is there a way, to read multivalue properties? Here is an example of the msip_labels property (GUIDs replaced with XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX):
msip_labels: MSIP_Label_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX_Enabled=true;
MSIP_Label_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX_SetDate=2019-11-14T07:16:38Z;
MSIP_Label_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX_Method=Privileged;
MSIP_Label_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX_Name=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX;
MSIP_Label_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX_SiteId=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX;
MSIP_Label_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX_ActionId=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX;
MSIP_Label_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX_ContentBits=1
msip_label_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX_enabled: true
msip_label_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX_setdate: 2019-11-14T07:16:48Z
msip_label_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX_method: Privileged
msip_label_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX_name:
XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
msip_label_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX_siteid:
XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
msip_label_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX_actionid:
XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
msip_label_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX_contentbits: 0
Finally I have figured out, how to create the property schema string, so it returns the right data type. Helpful for finding out the datatype was analyzing the item using Outlook Spy. The correct line of code with the right Schema String for querying msip_labels is:
var mSIPLabels = propertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/string/{00020386-0000-0000-C000-000000000046}/msip_labels/0x0000001F") as string;
after the property name, I had to pass the type descriptor 0x0000001F
And additional to this:
How to set a MIP Lablel to (Outlook VSTO) MailItem
public void SetMIP_LabelPublic(MailItem newMailItem)
{
var lblID = MipFileService.Label_Standard_Id; // <== your label ID
var tenantId = MipSettings.TenantId; //<== azur information tenant (your company) id
var mipMethod = "Privileged";
var dd = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.CreateSpecificCulture("en-us"));
var mipPropertyText = $"MSIP_Label_{lblID}_Enabled=true; "
+ $"MSIP_Label_{lblID}_SetDate={dd}; "
+ $"MSIP_Label_{lblID}_Method={mipMethod}; "
+ $"MSIP_Label_{lblID}_SiteId={tenantId}; ";
newMailItem.PropertyAccessor.SetProperty("http://schemas.microsoft.com/mapi/string/{00020386-0000-0000-C000-000000000046}/msip_labels/0x0000001F", mipPropertyText);
}
Hi I am working on a bulk email sending feature. Below is my loop that validates email and sends them to each recipient:
foreach (var userID in recipientUserIds)
{
var userInfo = //getting from database using userID.
try
{
to = new MailAddress(userInfo.Email1, userInfo.FirstName + " " + userInfo.LastName);
}
catch (System.FormatException fe)
{
continue;
}
using (MailMessage message = new MailMessage(from, to))
{
//populate message and send email.
}
}
Since the recipientUserIds is always more than 2000, using try-catch seems to be very expensive for each user in this case just to validate the email address format. I was wondering to use regex, but not sure if that will help with performance.
So my question is if there is any better or performance optimized way to do the same validation.
Validating email addresses is a complicated task, and writing code to do all the validation up front would be pretty tricky. If you check the Remarks section of the MailAddress class documentation, you'll see that there are lots of strings that are considered a valid email address (including comments, bracketed domain names, and embedded quotes).
And since the source code is available, check out the ParseAddress method here, and you'll get an idea of the code you'd have to write to validate an email address yourself. It's a shame there's no public TryParse method we could use to avoid the exception being thrown.
So it's probably best to just do some simple validation first - ensure that it contains the minimum requirements for an email address (which literally appears to be user#domain, where domain does not have to contain a '.' character), and then let the exception handling take care of the rest:
foreach (var userID in recipientUserIds)
{
var userInfo = GetUserInfo(userID);
// Basic validation on length
var email = userInfo?.Email1?.Trim();
if (string.IsNullOrEmpty(email) || email.Length < 3) continue;
// Basic validation on '#' character position
var atIndex = email.IndexOf('#');
if (atIndex < 1 || atIndex == email.Length - 1) continue;
// Let try/catch handle the rest, because email addresses are complicated
MailAddress to;
try
{
to = new MailAddress(email, $"{userInfo.FirstName} {userInfo.LastName}");
}
catch (FormatException)
{
continue;
}
using (MailMessage message = new MailMessage(from, to))
{
// populate message and send email here
}
}
How do we show the pictures of the requried and optional attendees in an appointment? According to the documentation it should be possible retrive the photos with this method (MSDN):
private static void GetContactPhoto(ExchangeService service, string ItemId)
{
// Bind to an existing contact by using the ItemId passed into this function.
Contact contact = Contact.Bind(service, ItemId);
// Load the contact to get access to the collection of attachments.
contact.Load(new PropertySet(ContactSchema.Attachments));
// Loop through the attachments looking for a contact photo.
foreach (Attachment attachment in contact.Attachments)
{
if ((attachment as FileAttachment).IsContactPhoto)
{
// Load the attachment to access the content.
attachment.Load();
}
}
FileAttachment photo = contact.GetContactPictureAttachment();
// Create a file stream and save the contact photo to your computer.
using (FileStream file = new FileStream(photo.Name, FileMode.Create, System.IO.FileAccess.Write))
{
photo.Load(file);
}
}
But when loading an appointment object the RequiredAttendees and OptionalAttendees arrays only returns EmailAdress object. How can the EmailAdress be converted into an ItemId so that the GetContactsPhoto method in the documentation can be used?
//Print out to se what we get.
foreach (Microsoft.Exchange.WebServices.Data.Appointment a in appointments)
{
a.Load(_customPropertySet);
//Required, Optional, Resource
// Check responses from required attendees.
for (int i = 0; i < a.RequiredAttendees.Count; i++)
{
Console.WriteLine("Required attendee - " + a.RequiredAttendees[i].Address);
}
The customPropertySet looks like this:
//Configure so that the body of an appointment is readable.
PropertySet _customPropertySet = new PropertySet(BasePropertySet.FirstClassProperties,
AppointmentSchema.MyResponseType,
AppointmentSchema.IsMeeting,
AppointmentSchema.ICalUid,
AppointmentSchema.RequiredAttendees,
AppointmentSchema.OptionalAttendees,
AppointmentSchema.Resources);
Tried passing the email address as Id but that returns an error message saying that the "id is malformed".
Update 1: Tried using Resolve method as pointed out in the answer by Jason Johnson. He correctly pointed out and I can see after testing that it's only Contacts that are in my mailbox that get's resolved. That's not what we need we need the Picture of the user in Exchange or AD.
The contacts your code works with are personal contacts (the ones stored in the user's Contacts folder in their mailbox). I point this out because only attendees for which the user actually has a contact will work with this method. That being said, you should be able to use ResolveNames to resolve the email address to a contact ID.
For attendees that are part of the user's Exchange organization, you need to get photo information from AD or Exchange itself. See https://msdn.microsoft.com/EN-US/library/office/jj190905(v=exchg.150).aspx.
If you are on Exchange 2013 or Office 365, you may be able to use FindPeople API to get the contact using the email address for your recipients.
I have created an Outlook 2007 add-in in C#.NET 4.0.
I want to read the safe sender list in my C# code.
if (oBoxItem is Outlook.MailItem)
{
Outlook.MailItem miEmail = (Outlook.MailItem)oBoxItem;
OlDefaultFolders f = Outlook.OlDefaultFolders.olFolderContacts;
if (miEmail != null)
{
string body = miEmail.Body;
double score = spamFilterObject.CalculateSpamScore(body);
if (score <= 0.9)
{
miEmail.Move(mfJunkEmail);
}
}
}
So, the above code moves all email to spam, even though they are present in the safe sender list. Thus I want to get the safe sender list so that I can avoid this spam checking.
Could anybody please help me on this?
The Outlook object model doesn't expose these lists (for more or less obvious reasons). The safe sender list can be read straight from the registry at:
HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles\[PROFILE NAME]\0a0d020000000000c000000000000046\001f0418
This binary registry key contains double-byte characters, separated by a semicolon (;).
The MAPI property mapping onto this registry key is
PR_SPAM_TRUSTED_SENDERS_W, documented here.
Chavan, I assume since this hasn't been updated in over 4 years, you don't need any more information, but this question and the answer helped me find what I was looking for (it was very hard to find) and enabled me to write the code below that may help if you're still looking for an answer.
This code runs in LINQPad, so if you aren't a LINQPad user, remove the .Dump() methods and replace with Console.WriteLine or Debug.WriteLine.
Cheers!
const string valueNameBlocked = "001f0426";
const string valueNameSafe = "001f0418";
// Note: I'm using Office 2013 (15.0) and my profile name is "Outlook"
// You may need to replace the 15.0 or the "Outlook" at the end of your string as needed.
string keyPath = #"Software\Microsoft\Office\15.0\Outlook\Profiles\Outlook";
string subKey = null;
var emptyBytes = new byte[] { };
var semi = new[] { ';' };
string blocked = null, safe = null;
// I found that my subkey under the profile was not the same on different machines,
// so I wrote this block to look for it.
using (var key = Registry.CurrentUser.OpenSubKey(keyPath))
{
var match =
// Get the subkeys and all of their value names
key.GetSubKeyNames().SelectMany(sk =>
{
using (var subkey = key.OpenSubKey(sk))
return subkey.GetValueNames().Select(valueName => new { subkey = sk, valueName });
})
// But only the one that matches Blocked Senders
.FirstOrDefault(sk => valueNameBlocked == sk.valueName);
// If we got one, get the data from the values
if (match != null)
{
// Simultaneously setting subKey string for later while opening the registry key
using (var subkey = key.OpenSubKey(subKey = match.subkey))
{
blocked = Encoding.Unicode.GetString((byte[])subkey.GetValue(valueNameBlocked, emptyBytes));
safe = Encoding.Unicode.GetString((byte[])subkey.GetValue(valueNameSafe, emptyBytes));
}
}
}
// Remove empty items and the null-terminator (sometimes there is one, but not always)
Func<string, List<string>> cleanList = s => s.Split(semi, StringSplitOptions.RemoveEmptyEntries).Where(e => e != "\0").ToList();
// Convert strings to lists (dictionaries might be preferred)
var blockedList = cleanList(blocked).Dump("Blocked Senders");
var safeList = cleanList(safe).Dump("Safe Senders");
byte[] bytes;
// To convert a modified list back to a string for saving:
blocked = string.Join(";", blockedList) + ";\0";
bytes = Encoding.Unicode.GetBytes(blocked);
// Write to the registry
using (var key = Registry.CurrentUser.OpenSubKey(keyPath + '\\' + subKey, true))
key.SetValue(valueNameBlocked, bytes, RegistryValueKind.Binary);
// In LINQPad, this is what I used to view my binary data
string.Join("", bytes.Select(b => b.ToString("x2"))).Dump("Blocked Senders: binary data");
safe = string.Join(";", safeList) + ";\0"; bytes = Encoding.Unicode.GetBytes(safe);
string.Join("", bytes.Select(b => b.ToString("x2"))).Dump("Safe Senders: binary data");
PST and IMAP4 (ost) stores keep the list in the profile section in the registry. Profile section guid is {00020D0A-0000-0000-C000-000000000046}. To access the data directly, you will need to know the Outlook version and the profile name.
Exchange store keeps this data as a part of the server side rule that processes incoming messages on the server side. You can see the rule data in OutlookSpy (I am its author) - go to the Inbox folder, "Associated Contents" tab, find the entry named (PR_RuleMsgName) == "Junk E-mail Rule", double click on it, take a look at the PR_EXTENDED_RULE_CONDITION property.
Outlook Object Model does not expose Junk mail settings. If using Redemption (I am also its author) is an option, it exposes the RDOJunkEmailOptions.TrustedSenders collection (works both for the PST and Exchange stores):
set Session = CreateObject("Redemption.RDOSession")
Session.MAPIOBJECT = Application.Session.MAPIOBJECT
set Store = Session.Stores.DefaultStore
set TrustedSenders = Store.JunkEmailOptions.TrustedSenders
for each v in TrustedSenders
debug.print v
next