Exchange Changing Email, How to reread email

Add-in Express™ Support Service
That's what is more important than anything else

Exchange Changing Email, How to reread email
After obtaining a MailItem, it is changed by the server. How can I reload the new version? 
Mark McCray




Posts: 17
Joined: 2010-04-15
Hi All,

I have an application that allows the user to save sent emails to the database. As recommended, I process the email when it is added to the sent items folders. I use a filter to get a list of emails I have not saved and process each one by prompting the user for the save location and then save the email and add a user property that it has been saved. I am randomly getting the "The operation cannot be performed because the message has been changed" error when I try to add the property and I think I have figured out why, but not sure how to handle.

Between the time I load the MailItem (to show the subject) and the user completes the form, the MailItem is getting changed, I am fairly certain by the server. There are properties being added to the email for "EntityExtractionSuccess", "EntityExtractionServiceDiagnosticContext", and "TeeVersion". No events are being triggered in Outlook when the message is changed.

Since the Exchange is Office 365, I assume these are indexing or other Microsoft background processes doing this. The email is changed within 1 minute of the item being added to sent items. Subsequent reads, after the "change" work fine.

Waiting a minute is not an option. I need to catch the error, reload the "changed" email, and then do my save. I have also considered skipping the email until I see the new properties, but this is not happening at every user site and until I can identify the exact process that is modifying the MailItem, it would be best just to handle the error.

With the background out of the way, here are some questions I hope you can help with.

1. Is there a way to fetch/read the new version of the MailItem? I cannot find any method that will do that.
2. Does anyone have any information on the properties mentioned above or what could be adding them?

I will provide code snippets if needed.

Thanks,

Mark
Posted 03 Feb, 2020 15:54:09 Top
Andrei Smolin


Add-in Express team


Posts: 17132
Joined: 2006-05-11
Hello Mark,

Mark McCray writes:
Between the time I load the MailItem (to show the subject) and the user completes the form, the MailItem is getting changed, I am fairly certain by the server.


You can retrieve the subject and other details (and EntryId), release the email (via Marshal.ReleaseComObject) and show the details to the user. When the user provides required data, you get the email via Namespace.GetEmailFromId({the EntryId}, [{the Store ID}]), modify it and save. If an error occurs while you update the email, skip the remaining steps, release every little thing, wait for a while and retry.

Regards from Belarus (GMT+3),

Andrei Smolin
Add-in Express Team Leader
Posted 04 Feb, 2020 02:37:19 Top
Andrei Smolin


Add-in Express team


Posts: 17132
Joined: 2006-05-11
Sorry, GetEmailFromId --> GetItemFromId.

Regards from Belarus (GMT+3),

Andrei Smolin
Add-in Express Team Leader
Posted 04 Feb, 2020 02:49:19 Top
Mark McCray




Posts: 17
Joined: 2010-04-15
Thank You! That is exactly what I needed. You guys are always helpful.

If I may bother you for one other question regarding releasing.

Take this Code snippet: (c#)
  
      string criteria = $"[{UserProperties.PROP_PROMPTTOSAVE}] = true";
      MSOutlook.MAPIFolder sentFolder = OutlookApp.Session.GetDefaultFolder(MSOutlook.OlDefaultFolders.olFolderSentMail);
      AllItems = sentFolder.Items;
      FoundItems = AllItems.Restrict(criteria);
      int cnt = FoundItems.Count;



I then iterate the found items using a while loop and FindNext(). I release each Item in FoundItems after processing.

In this scenario, do I need to iterate all the instances in AllItems and release them, or can I just release AllItems?

I could make the code this:

FoundItems = sentFolder.Items.Restrict(criteria);


But does that give me an Implicit reference to "Items" that I need to iterate through.

This is important because as you might imagine, SentFolder.Items can contain thousands of items....

Thank you again!

Mark
Posted 04 Feb, 2020 10:25:29 Top
Andrei Smolin


Add-in Express team


Posts: 17132
Joined: 2006-05-11
Hello Mark,

Mark McCray writes:
In this scenario, do I need to iterate all the instances in AllItems and release them, or can I just release AllItems?


AllItems only. The idea is: every time (there are a couple of really rare exceptions) you access an Outlook property/method that returns a value of an object type, that value is actually a COM object: OutlookApp.Session, {an Outlook.Namespace object}.GetDefaultFolder(...), sentFolder.Items, {an Outlook.Items object}.Restrict(), Items[i] - all of these return COM objects. Since Outlook is known for its misbehavior when a COM object is left unreleased, we suggest releasing every COM object *you* create (in the above way). You shouldn't release COM objects that Add-in Express passes to your event handlers (those that are connected to Add-in Express events only). Say, you don't release the folder object passed to you in a folder event such as ExplorerFolderSwitch.

Mark McCray writes:
FoundItems = sentFolder.Items.Restrict(criteria);


Since calling sentFolder.Items creates a COM object, the construct above doesn't let you release it.

This isn't about the number of items in the folder: the COM object should be created more or less quickly. Restricting may work more slowly. Still scanning the items may work far more slowly.

If the speed is of importance, consider using MAPIFolder.GetTable; see https://docs.microsoft.com/en-us/office/vba/api/outlook.folder.gettable. If you choose this way, consider getting all the information in one go using Table.GetArray(); see https://docs.microsoft.com/en-us/office/vba/api/outlook.table.getarray. Note that you may choose the table (array) to only contain EntryID and the UserProperty; this should work faster. When you need more data from the item, retrieve the item using GetItemFormId().

Regards from Belarus (GMT+3),

Andrei Smolin
Add-in Express Team Leader
Posted 05 Feb, 2020 03:29:15 Top
Mark McCray




Posts: 17
Joined: 2010-04-15
Thanks Andrei, I am trying the GetTable() options.

But I am still not clear on one thing. I understand that if I create an object, I need to release it. But if a call results in me creating a collection, do I have to release each item in the collection even if I don't access it? I think the answer is yes, but clarity, consider the two options below:


				string criteria = $"[{UserProperties.PROP_PROMPTTOSAVE}] = true";
                AllItems = sentFolder.Items;
                FoundItems = AllItems.Restrict(criteria);
                int cnt = FoundItems.Count;

                //Process each item in found items and release each "item"

                //#1
                for (int i = 0; i < AllItems.Count; i++)
                {
                    Marshal.ReleaseComObject(AllItems[i]);
                }

                //#2 
                for (int i = 0; i < sentFolder.Items.Count; i++)
                {
                    Marshal.ReleaseComObject(sentFolder.Items[i]);
                }
				



Do I need both #1 and #2?

Thanks again
Mark
Posted 05 Feb, 2020 16:14:58 Top
Andrei Smolin


Add-in Express team


Posts: 17132
Joined: 2006-05-11
Mark McCray writes:
do I have to release each item in the collection even if I don't access it?


No. Unless you you create a COM object by accessing an item of that collection.

Here's how this work:
string criteria = $"[{UserProperties.PROP_PROMPTTOSAVE}] = true"; 
AllItems = sentFolder.Items; 
FoundItems = AllItems.Restrict(criteria); 
Marshal.ReleaseComObject(AllItems); AllItems = null;
              
for (int i = 1; i <= FoundItems.Count; i++) 
{ 
    object item = nulll
    try 
    {
        item = FoundItems[i];
        // handle the item. Say, here you cast item to Outlook.MailItem and 
        // retrieve/set its properties
    }                
    catch (ex as System.Exception)
    {
        // TODO log the exception
    }
    finally
    {
        if (item !=null) Marshal.ReleaseComObject(item); item = null;
    }               
} 
Marshal.ReleaseComObject(FoundItems); FoundItems = null;


Notes.
1. Creating a collection object doesn't mean creating COM objects for collection items.
2. COM collections start from 1, not 0.
3. Casting a variable pointing to a COM object doesn't create another COM object. Instead, both variables point to the same COM object, so that you need to release it once.
4. You also should use a try/catch/finally to be able to release every COM object in case of an exception.
5. Log exceptions. this lets you find issues early.

Regards from Belarus (GMT+3),

Andrei Smolin
Add-in Express Team Leader
Posted 06 Feb, 2020 02:04:37 Top
Mark McCray




Posts: 17
Joined: 2010-04-15
Thanks Andrei,

Note #1 was the point I was unsure of.


Thanks again for the help!
Posted 06 Feb, 2020 08:44:01 Top
Andrei Smolin


Add-in Express team


Posts: 17132
Joined: 2006-05-11
Welcome!

Regards from Belarus (GMT+3),

Andrei Smolin
Add-in Express Team Leader
Posted 06 Feb, 2020 09:08:02 Top