I want to add a "resend" context menu in my Outlook 2016 add-in, to resend an email. The original email should be re-displayed to the user for him to make any modifications if necessary, and then press the 'send' button. It seems that I need to create a copy of the email, as calling Display() on the original message (or a copy created with MailItem.Copy()) just views the message, as opposed to showing it editable with a send button.
I got this so far - pretty straight forward:
Outlook.MailItem clone = Globals.ThisAddIn.Application.CreateItem(Outlook.OlItemType.olMailItem) as Outlook.MailItem;
clone.SendUsingAccount = email.SendUsingAccount;
clone.To = email.To;
clone.CC = email.CC;
clone.BCC = email.BCC;
clone.Subject = email.Subject;
clone.Body = email.Body;
clone.HTMLBody = email.HTMLBody;
for (int i = 1; i <= email.Attachments.Count; ++i)
clone.Attachments.Add(email.Attachments[i], email.Attachments[i].Type, email.Attachments[i].Position, email.Attachments[i].DisplayName);
However, I am getting a DISP_E_MEMBERNOTFOUND error when trying to copy the attachments. What am I doing wrong?
Attachments.Add only allows to pass a string pointing to a fully qualified path to a file or an Outlook item (such as MailItem). Also note that you code only copies the recipient display names, which may or may not be successfully resolved.
Outlook Object Model exposes MailItem.Copy method, but it creates a copy in the same sent/unsent state as the original.
If using Redemption (I am its author) is an option, you can use RDOMail.CopyTo() method - it will copy all the properties and sub-objects (such as recipients and attachments) but it will leave the sent state intact (since in MAPI it can only be set before the message is saved for the very first time).
Off the top of my head:
using Redemption;
...
RDOSession session = new RDOSession();
session.MAPIOBJECT = Globals.ThisAddIn.Application.Session.MAPIOBJECT;
RDOMail clone = session.GetDefaultFolder(rdoDefaultFolders.olFolderDrafts).Items.Add();
RDOMail original = (RDOMail)session.GetRDOObjectFromOutlookObject(email);
original.CopyTo(clone);
clone.Save();
MailItem OutlookClone = Globals.ThisAddIn.Application.Session.GetItemFromID(clone.EntryID);
OutlookClone.Display()
Related
We are developing an Outlook VSTO add-in.
Right now I am trying to append some information to a meeting invite the user is in the process of composing. I want the content to appear in the body like what clicking the Teams-meeting button would do, where formatted text and links are appended to the end of the body.
Since the content is HTML and the Outlook Object Model does not expose an HTMLBody property for AppointmentItems, I try to set it via Redemption:
// Dispose logic left out for clarity but everything except outlookApplication and outlookAppointment is disposed after use
Application outlookApplication = ...;
AppointmentItem outlookAppointment = ...; // taken from the open inspector
NameSpace outlookSession = outlookApplication.Session;
RDOSession redemptionSession = RedemptionLoader.new_RDOSession();
redemptionSession.MAPIOBJECT = outlookSession.MAPIOBJECT;
var rdoAppointment = (RDOAppointmentItem)redemptionSession.GetRDOObjectFromOutlookObject(outlookAppointment);
string newBody = transform(rdoAppointment.HTMLBody); // appends content right before HTML </body> tag
rdoAppointment.BodyFormat = (int)OlBodyFormat.olFormatHTML;
rdoAppointment.HTMLBody = newBody;
Problem
The Outlook inspector window is not updating with the appended content. If I try to run the code again, I can see the appended content in the debugger, but not in Outlook.
Things I have tried:
Saving the RDOAppointmentItem
Also adding the content to Body property
Using SafeAppointmentItem instead of RDOAppointmentItem; didn't work because HTMLBody is a read-only property there
Setting PR_HTML via RDOAppointment.Fields
Paste the HTML via WordEditor (see below)
Attempt to use WordEditor
Per suggestion I also attempted to insert the HTML via WordEditor:
// Dispose logic left out for clarity but everything except inspector is disposed after use
string htmlSnippet = ...;
Clipboard.SetText(htmlSnippet, TextDataFormat.Html);
Inspector inspector = ...;
Document wordDoc = inspector.WordEditor;
Range range = wordDoc.Content;
range.Collapse(WdCollapseDirection.wdCollapseEnd);
object placement = WdOLEPlacement.wdInLine;
object dataType = WdPasteDataType.wdPasteHTML;
range.PasteSpecial(Placement: ref placement, DataType: ref dataType);
... but I simply receive the error System.Runtime.InteropServices.COMException (0x800A1066): Kommandoen lykkedes ikke. (= "Command failed").
Instead of PasteSpecial I also tried using PasteAndFormat:
range.PasteAndFormat(WdRecoveryType.wdFormatOriginalFormatting);
... but that also gave System.Runtime.InteropServices.COMException (0x800A1066): Kommandoen lykkedes ikke..
What am I doing wrong here?
EDIT: If I use Clipboard.SetText(htmlSnippet, TextDataFormat.Text); and then use plain range.Paste();, the HTML is inserted at the end of the document as intended (but with the HTML elements inserted literally, so not useful). So the general approach seems to be okay, I just can't seem to get Outlook / Word to translate the HTML.
Version info
Outlook 365 MSO 32-bit
Redemption 5.26
Since the appointment is being displayed, work with the Word Object Model - Inspector.WordEditor returns the Document Word object.
Per Dmitrys suggestion, here is a working solution that:
Shows the inserted content in the inspector window.
Handles HTML content correctly with regards to both links and formatting (as long as you stay within the limited capabilities of Words HTML engine).
using System;
using System.IO;
using System.Text;
using Outlook = Microsoft.Office.Interop.Outlook;
using Word = Microsoft.Office.Interop.Word;
namespace VSTO.AppendHtmlExample
{
public class MyExample
{
public void AppendAsHTMLViaFile(string content)
{
// TODO: Remember to release COM objects range and wordDoc and delete output file in a finally clause
Outlook.Inspector inspector = ...;
string outputFolderPath = ...;
string outputFilePath = Path.Combine(outputFolderPath, "append.html");
Word.Document wordDoc = inspector.WordEditor;
File.WriteAllText(outputFilePath, $"<html><head><meta charset='utf-8'/></head><body>{content}</body></html>", Encoding.UTF8);
Word.Range range = wordDoc.Content;
range.Collapse(Word.WdCollapseDirection.wdCollapseEnd);
object confirmConversions = false;
object link = false;
object attachment = false;
range.InsertFile(fileName,
ConfirmConversions: ref confirmConversions,
Link: ref link,
Attachment: ref attachment);
}
}
}
I have this script that finds a paragraph style, puts an item from a library at the very end and applies object style:
myDoc = app.documents[0];
myLib = app.libraries[0];
myObjectStyle = myDoc.objectStyles.item ("marker");
app.findTextPreferences = app.changeTextPreferences = null;
app.findTextPreferences.appliedParagraphStyle = "Custom"
var myFound = app.activeDocument.findText(true);
alert (myFound.length);
try {
for (i = 0; i < myFound.length; i++) {
myIcon = myLib.assets.itemByName("winieta_tr").placeAsset (myFound[i].insertionPoints[-2])[0];
myIcon.appliedObjectStyle = myObjectStyle;
// myFound[i].remove ();
}
}
catch (e) {alert (e.message)}
I don't know how to alter it, so the items are obtained not from library but form pasteboard - any help would be appreciated.
Is it possible to find elements that are in the document by name, as it is with library elements?
Yes, you can find an object by name (you would assign that name in the layers panel) simply by using
myDoc.pageItems.itemByName("myItemName");
If you are looking for the same thing on a specific spread (for example if several items on several spreads have the same name), you can use
myDoc.spreads[0].pageItems.itemByName("myItemName");
Or if you just want to use the currently active spread
app.activeWindow.activeSpread.pageItems.itemByName("myItemName");
Just make sure not to use the page to address a page item on the pasteboard as the pasteboard does not belong to any page.
Is it possible to find elements that are in the document by name, as it is with library elements?
You can apply a script labels to the frame on the pasteboard to give it a name.
i am creating an Outlook add-in where i need to enable Conversation view in inbox folder("Show as Conversation") in outlook.i tried through registry Key ("Upgrade To Conversations"),but still i didn't get that.
i tried as following
RegistryKey rkconversations = Registry.CurrentUser.CreateSubKey(#"Software\Microsoft\Office" + OLVersion + #"\Outlook\Setup");
rkconversations.SetValue("UpgradeToConversations", "1", RegistryValueKind.DWord);
i aslo tried like this:
Outlook.Views views = inbox.Views;
Outlook.View view = views["Hide Reading Pane"];
if (view != null)
view.Delete();
Outlook.View view1 = views.Add("Hide Reading Pane", Outlook.OlViewType.olTableView,
Outlook.OlViewSaveOption.olViewSaveOptionThisFolderOnlyMe);
tableView = view1 as Outlook.TableView;
tableView.ShowReadingPane = false;
tableView.ShowConversationByDate = true;
tableView.ShowConversationSendersAboveSubject = true;
tableView.ShowFullConversations = true;
view1.Save();
view1.Apply();
Show as Conversations is not enabled
Try to do the required modifications in Outlook manually. Then take a look at the XML property of the View/TableView object and compare it with your own. Thus, you may find the missed point.
We have developed a Thunderbird (11) plugin that allows us to save the content of a message to disk. Now we are extending this extension to allow automatic processing of a message when you close it. We run into a number of issues:
We cannot find a way to hook into a 'close tab' event. We are also having trouble getting the Message URI of the currently open tabs (we are trying catching click and keyboard events now). This information does not appear to be available in the DOM of the tab container.
Is there a way to detect closing of a mail message tab or window in a generic way, together with retrieving the URI of the closed mail message for further processing?
We have looked at the documentation of the tab container, the NsIWindowMediator, tried various event listeners, but no luck so far.
Edit: We are getting some results using the most recently closed tabs list. Not a very elegant solution but at least we have a reference to the tab. Now we only have to get the URI to the message that was contained inside the tab.
We cannot find a way to hook into a 'close tab' event.
The (badly documented) <tabmail> element allows registering tab monitors. Something like this should work:
var tabmail = document.getElementById("tabmail");
var monitor = {
onTabClosing: function(tab)
{
...
}
};
tabmail.registerTabMonitor(monitor);
We are also having trouble getting the Message URI of the currently open tabs
The <tabmail> element has a property tabInfo containing information on the currently open tabs. You probably want to look only at the tabs where mode.name is "message" (there is a bunch of other modes as well, e.g. "folder" or "contentTab"). This mode has a getBrowser() method, so something like this should do:
var tabmail = document.getElementById("tabmail");
for (var i = 0; i < tabmail.tabInfo.length; i++)
{
var tab = tabmail.tabInfo[i];
if (tab.mode.name == "message")
alert(tab.mode.getBrowser().currentURI.spec);
}
Edit: As Peter points out in the comments, the approach to get the URI for a message will only work the currently loaded message - all tabs reuse the same browser element for the mail messages. Getting the URI properly is more complicated, you have to get the nsIMsgDBHdr instance for the message via TabInfo.folderDisplay.selectedMessage and then use nsIMsgFolder.getUriForMsg() to construct the URI for it:
var tabmail = document.getElementById("tabmail");
for (var i = 0; i < tabmail.tabInfo.length; i++)
{
var tab = tabmail.tabInfo[i];
if (tab.mode.name != "message")
continue;
var message = tab.folderDisplay.selectedMessage;
alert(message.folder.getUriForMsg(message));
}
For the second part of the question:
The following example code will at provide you the msgDBHdr objects of all opened tabs. You should do some checks on the type to avoid accessing a message in a calendar tab.):
tabInfos = window.document.getElementById("tabmail").tabInfo;
for (i = 0; i < tabInfos.length; i++) {
msgHdr = tabInfos[i].folderDisplay.selectedMessage;
alert(
msgHdr.mime2DecodedSubject+"\n"
+msgHdr.messageId+"\n"
+"in view type "+tabInfos[i].mode.type
);
}
The tabinfo entries have some further interesting information. Just open the ErrorConsole and run
top.opener.window.document.getElementById("tabmail").tabInfo[0].toSource()
and read through it carefully.
I'm new to Outlook add-in programming and not sure if this is possible:
I want to display a pop-up form (or selection) and ask for user input at the time they click Send. Basically, whenever they send out an email (New or Reply), they will be asked to select a value in a dropdown box (list items from a SQL database, preferrably).
Base on their selection, a text message will be appended to the mail's subject.
I did my research and it looks like I should use Form Regions but I'm not sure how can I display a popup/extra form when user click Send.
Also, it looks like Form Regions can be used to extend/replace current VIEW mail form but can I use it for CREATE NEW form?
Thanks for everybody's time.
You can probably add the Item Send event handler in the ThisAddIn Internal Startup method and then in the Item Send Event, call the custom form (a windows form).
In the below sample I call a custom windows form as modal dialog before the email item is send and after the send button is clicked.
private void InternalStartup()
{
this.Application.ItemSend += new ApplicationEvents_11_ItemSendEventHandler(Application_ItemSend);
}
void Application_ItemSend(object Item, ref bool Cancel)
{
if (Item is Microsoft.Office.Interop.Outlook.MailItem)
{
Microsoft.Office.Interop.Outlook.MailItem currentItem = Item as Microsoft.Office.Interop.Outlook.MailItem;
Cancel = true;
Forms frmProject = new ProjectForm();;
DialogResult dlgResult = frmProject.ShowDialog();
if (dlgResult == DialogResult.OK)
System.Windows.Forms.SendKeys.Send("%S"); //If dialog result is OK, save and send the email item
else
Cancel = false;
currentItem.Save();
currentItem = null;
}
}