VSTO Outlook Plugin: Cannot get AppointmentItem in Item_Change event when recurring appointment is dragged and dropped by user - vsto

I want to catch the change event on an AppointmentItem. I use Outlook 2017 for tests.
To achieve I use:
I attached the events like this:
public void AttachEvents()
{
_CalendarItems.ItemAdd += Item_Add;
_CalendarItems.ItemChange += Item_Change;
_DeletedItems.ItemAdd += Item_Delete_Add;
The Item_Change method looks like this:
public void Item_Change(Object item)
{
if (item != null && item is Outlook.AppointmentItem)
{
Outlook.AppointmentItem myAppointment = item as Outlook.AppointmentItem;
To test the code I created a recurring appointment series. I double-clicked on appointment in the calendar and entered some title and body and saved.
Now I started my code and inspected the item.
Unfortunately, item points to the series and NOT to the individual appointment when item changed is initiated.
How can I retrieve the actual AppointmentItem when Item_Changed is initiated?
Related Stackoverflow Posting: Outlook Addin: Moving Appointment in Calendar does not reflect new date/time in AppointmentItem (catch Calendar.ItemChange) But still there is no solution to this
More on this topic:
https://www.add-in-express.com/forum/read.php?FID=5&TID=15384
https://social.msdn.microsoft.com/Forums/sqlserver/en-US/4ec55891-fb64-408f-b1cf-4bf05765b866/outlook-get-original-time-of-recurring-exception-item-that-is-opened-with-drag-drop?forum=vsto

Exceptions are not actual appointments - they are stored as embedded message attachments on the master appointments. You get the master appointment, and you would need to access its exceptions to see what changed.

There is a tricky solution to that.
Assumption: The user uses the calendar view to change the item. As the event is thrown by the calendar view this should be true in all cases:
_CalendarItems = calendarFolder.Items;
_CalendarItems.ItemChange += Item_Change;
[...]
now we can use the CalendarView to calculate the selected Startdate and compare it to all Exceptions stored in the RecurrencePattern ...
if (myAppointment.IsRecurring)
{
// in case of recurring appointments at this point we always get
// only a reference to the series master NOT the occurrence
// Assumption: The user clicked on the AppointmentItem in the calendar view
// So we can calculate the selected Start Time from this selection range
// then compare this against all Exceptions in the OccurrencePattern of the recurring pattern
// if we find one AppointmentItem in the Exceptions which has the same DateTime then we found the correct one.
//
Outlook.Application application = new Outlook.Application();
Outlook.Explorer explorer = application.ActiveExplorer();
Outlook.Folder folder = explorer.CurrentFolder as Outlook.Folder;
Outlook.View view = explorer.CurrentView as Outlook.View;
// get the current calendar view
if (view.ViewType == Outlook.OlViewType.olCalendarView)
{
Outlook.CalendarView calView = view as Outlook.CalendarView;
Outlook.RecurrencePattern pattern = myAppointment.GetRecurrencePattern();
for (int i = 1; i <= pattern.Exceptions.Count; i++)
{
Outlook.Exception myException = pattern.Exceptions[i];
Outlook.AppointmentItem exceptionItem = myException.AppointmentItem;
DateTime itemDateStart = exceptionItem.Start;
if (itemDateStart == calView.SelectedStartTime)
{
updateMyPluginMeeting(exceptionItem);
return; // the use may only select on AppointmentItem so we can skip the rest
}
}
}
}
If you know any better solution to this let me know.

Related

Using mailitem.PrintOut() to print a single page?

I'm working on a simple Outlook 2016/2019 VSTO plugin.
When an email is selected and a ribbon button is pressed, it needs to print just the first page of the email to the default printer. mailitem.PrintOut(); works, but will print the whole email. Is there a way to specify the first page only?
var m = e.Control.Context as Inspector;
var mailitem = m.CurrentItem as MailItem;
if (mailitem != null)
{
mailitem.PrintOut();
}
Update: See my answer for the code I used to get this working.
The Outlook object model doesn't provide any property or method for that. You need to parse the message body on your own and use .net mechanisms for printing this piece on your own.
Note, you may try using the Word object model for printing the message bodies (a specific range of pages). The Document.PrintOut method prints all or part of the specified document. Optional parameters allow specifying the page range.
The Outlook object model provides three main ways for working with item bodies:
Body - a string representing the clear-text body of the Outlook item.
HTMLBody - a string representing the HTML body of the specified item.
Word editor - the Microsoft Word Document Object Model of the message being displayed. The WordEditor property of the Inspector class returns an instance of the Document class from the Word object model which you can use to deal with the message body.
You can read more about all these ways in the Chapter 17: Working with Item Bodies.
As #Eugene said, there's no way to specify a single page using mailItem.PrintOut.
I've finally managed to find a way to do this. I save the document as a .doc file in the temp directory, then using Microsoft.Office.Interop.Word to setup the page margins / size and then send the current page to the printer. Hopefully this helps someone as I couldn't find any working examples for c#!
private void btnPrintOnePage_Click(object sender, RibbonControlEventArgs e)
{
string randFile = Path.GetTempPath() + "POP_" + RandomString(35) + ".doc";
var m = e.Control.Context as Inspector;
var mailitem = m.CurrentItem as MailItem;
if (mailitem != null)
{
mailitem.SaveAs(randFile, OlSaveAsType.olDoc);
Word.Application ap = new Word.Application();
Word.Document document = ap.Documents.Open(randFile);
document.PageSetup.PaperSize = Word.WdPaperSize.wdPaperA4;
document.PageSetup.TopMargin = 25;
document.PageSetup.RightMargin = 25;
document.PageSetup.BottomMargin = 25;
document.PageSetup.LeftMargin = 25;
Word.WdPrintOutRange printRange = Word.WdPrintOutRange.wdPrintCurrentPage;
document.PrintOut(false,null,printRange);
document.Close(false, false, false);
File.Delete(randFile);
}
}
public static string RandomString(int length)
{
Random random = new Random();
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[random.Next(s.Length)]).ToArray());
}

Email sent from C# OOM stays in Outbox if Outlook is closed until next Outlook start

I'm trying to send emails from a .NET application using Outlook Object Model.
My application displays the Outlook message window so the user can see what we're sending and edit it first. When the user hits the Send button, the Outlook window closes, and the message gets sent. This works perfectly as long as the Outlook application is already running.
If the Outlook application isn't already running, the message gets stuck in the Outbox, and will not send until I start Outlook. When I start Outlook, I can see the message sitting in the Outbox folder for a few seconds, then it gets sent.
I need to show the New Message form to Outlook user to select the recipient(s) and possibly edit the message before sending.
Note: I know that this question was already asked here Email sent with Outlook Object Model stays in Outbox until I start Outlook
and the solution exists, but it is not provided (only the small hint is provided) and unfortunately I cannot ask for clarification / code example because I have not enough "reputation".
I tried to write my own implementation of the hint provided, but the SyncEnd event is fired only when Outlook is already open (just to remind, the question is about the case then Outlook is closed).
My code below. What is wrong?
using Microsoft.Office.Interop.Outlook;
using OutlookApp = Microsoft.Office.Interop.Outlook.Application;
class Mailer
{
AutoResetEvent mailSentEvent = new AutoResetEvent(false);
public void CreateMail()
{
OutlookApp outlookApp = null;
MailItem mailItem = null;
try
{
outlookApp = new OutlookApp();
mailItem = outlookApp.CreateItem(OlItemType.olMailItem);
mailItem.Subject = "Test Message";
mailItem.Body = "This is the message.";
string reportPath = #"C:\temp\aaaaa.pdf";
mailItem.Attachments.Add(reportPath);
mailItem.Display(true);
StartSync(outlookApp);
bool result = mailSentEvent.WaitOne();
}
catch (System.Exception)
{
throw;
}
finally
{
if (mailItem != null) Marshal.ReleaseComObject(mailItem);
if (outlookApp != null) Marshal.ReleaseComObject(outlookApp);
}
}
private static SyncObject _syncObject = null;
private void StartSync(OutlookApp outlookApp)
{
var nameSpace = outlookApp.GetNamespace("MAPI");
_syncObject = nameSpace.SyncObjects[1];
_syncObject.SyncEnd += new Microsoft.Office.Interop.Outlook.SyncObjectEvents_SyncEndEventHandler(OnSyncEnd);
_syncObject.Start();
}
private void OnSyncEnd()
{
mailSentEvent.Set();
}
}
the SyncEnd event is fired only when Outlook is already open
That is not true. The SyncObjects collection contains all Send\Receive groups. You need to iterate over all objects in the collection and call the Start method, for example:
Set sycs = nsp.SyncObjects
For i = 1 To sycs.Count
Set syc = sycs.Item(i)
strPrompt = MsgBox("Do you wish to synchronize " &; syc.Name &;"?", vbYesNo)
If strPrompt = vbYes Then
syc.Start
End If
Next

Creating a copy of MailItem and then deleting creates a new mail in the same folder

I created a copy of a mail in my Inbox folder by calling MailItem.copy() function and then I deleted the newly created mail item.
Following is sample code:
IDispatch * lDispatch;
_MailItemPtr lReadModeMailItem;
HRESULT lReturn = mMailItem->Copy(&lDispatch);
if (lReturn != S_OK)
{
return;
}
lReturn = lDispatch->QueryInterface(IID__MailItem, (LPVOID*)&lReadModeMailItem);
if (lReturn != S_OK || lReadModeMailItem == NULL)
{
return;
}
lReadModeMailItem->Close(olDiscard);
HRESULT lMyRet = lReadModeMailItem->Delete();
if (lMyRet != S_OK)
{
return;
}
lDispatch->Release();
After I am done executing this code, 2 new mail items are created in my Inbox folder.
Then, if I move to any other folder and come back to Inbox or restart Outlook, then 1 new mail item remains.
Why does 1 extra mail item remain in Inbox, even when I called MailItem.Delete() function?
How can I permanently delete the mail item created using MailItem.Copy() ?
That's the lack of IMAP accounts in Outlook.
You may consider using the following workarounds to get rid of such issues:
Programmatically switch folders in Outlook by setting the Explorer.CurrentFolder property which allows setting a Folder object that represents the current folder displayed in the explorer.
Use the SyncObject.Start method which begins synchronizing a user's folders using the specified Send\Receive group.

x-header missing for Outlook MeetingItems when received

I am using the following approach in a VSTO Outlook Addin (using Addin-Express library) to set a custom x-header attribute on mails and other items you can send from Microsoft Outlook, like meetings. It's a security classification which is evaluated by a mail gateway appliance for making sure encryption is activated on outgoing mails, depending on the classification. Inside the organization, it will just be displayed on the receiving side, which is Outlook 2016 desktop client.
Before sending, I set the x-header property like this:
string headerNamespace = "http://schemas.microsoft.com/mapi/string /{00020386-0000-0000-C000-000000000046}/";
public void SetHeader(PropertyAccessor acc, string header, string value)
{
acc.SetProperty(headerNamespace + "x-mycustomheader", value);
}
I always receive the header for mails, but not for Meetings. I know there is an associated appointment, but I tried reading the header from any object I could think of
On the receiving end, I get the current item from the explorer or inspector window and try to retrieve that header. The attribute is not visible in OutlookSpy and it's not inside the transport header. Is it possible that Outlook stored it somewhere else, or has it been removed? This process is working fine for MailItem types.
I stripped some parts like releasing the com objects for better readability. OutlookMeetingItem2 is a wrapper class. I have two alternative methods for reading the header, one is the PropertyAccessor and the other is used here, both can't find the header attribute.
using (OutlookMessageItemWrapper outlookItem = OutlookMessageItemFactory.GetMessageItem(currentItem))
{
object outlookitemMapi = outlookItem.MAPIOBJECT;
classificationString = mapi.GetHeader(outlookItem.MAPIOBJECT, Constants.CLASSIFICATIONHEADER);
if (classificationString == "" && outlookItem is OutlookMeetingItem2)
{
OutlookMeetingItem2 meetingItem = outlookItem as OutlookMeetingItem2;
MeetingItem meeting = meetingItem.Item;
object mapiObject = meeting.MAPIOBJECT;
classificationString = mapi.GetHeader(meeting.MAPIOBJECT, Constants.CLASSIFICATIONHEADER);
if (classificationString == "")
{
AppointmentItem appointment = meeting.GetAssociatedAppointment(false);
if (appointment != null)
{
classificationString = mapi.GetHeader(appointment.MAPIOBJECT, Constants.CLASSIFICATIONHEADER);
}
}
}
}

How to Accept and add Categories to RequiredAttendees Appointments using Exchange Web Services

I’m using ExchangeService(ExchangeVersion.Exchange2010_SP1)
I want to Accept and add Categories to RequiredAttendees Appointments. To do this i need to find these Appointments.
My understanding in EWS is that on Save of an Appointment that has RequiredAttendees a new Meeting Request is created for each of the ‘required attendees’.
How can I access the Appointments that were created automatically for the ‘required attendees’? These are shown in the required attendees Calendars as Appointments, along with a Meeting Request.
I have managed to do a crude find on the Subject (steps below)
Connect to server as Organiser
Create Appointment
Set Subject
Add Required Attendee
Save Appointment
Connect to server as Required Attendee from step 4
Find Appointment that has Subject at step 3
Add Categories to Appointment at step 7
Update Appointment at step 7
Accept Appointment at step 7
And this does work, but concerned users will change the Subject.
I have tried adding an Extended Property and value to the Appointment created by the Organiser and then FindItems for the Extended Property value in the Appointments connected as the Required Attendee. This does not work.
Is there a preferred method for what I’m trying to accomplish?
Thank you
Private Shared ReadOnly m_organiserEmailAddress As String = "Organiser#test.com"
Private Shared ReadOnly m_eventIdExtendedPropertyDefinition As New ExtendedPropertyDefinition(DefaultExtendedPropertySet.Meeting, "EventId", MapiPropertyType.Long)
'--> start here
Public Shared Function SaveToOutlookCalendar(eventCalendarItem As EventCalendarItem) As String
If eventCalendarItem.Id Is Nothing Then
'new
Dim newAppointment = EventCalendarItemMapper.SaveNewAppointment(eventCalendarItem)
'set the Id
eventCalendarItem.Id = newAppointment.Id.UniqueId.ToString()
'accept the calendar item on behalf of the Attendee
EventCalendarItemMapper.AcceptAppointmentAsAttendees(newAppointment)
Return eventCalendarItem.Id
Else
'update existing appointment
Return EventCalendarItemMapper.UpdateAppointment(eventCalendarItem)
End If
End Function
Private Shared Sub ConnectToServer(Optional autoUser As String = "")
If autoUser = "" Then
_service.Url = New Uri(ExchangeWebServicesUrl)
Else
_service.AutodiscoverUrl(autoUser)
End If
End Sub
Private Shared Sub ImpersonateUser(userEmail As String)
_service.Credentials = New NetworkCredential(ImpersonatorUsername, ImpersonatorPassword, Domain)
_service.ImpersonatedUserId = New ImpersonatedUserId(ConnectingIdType.SmtpAddress, userEmail)
End Sub
Private Shared Function SaveNewAppointment(eventCalendarItem As EventCalendarItem) As Appointment
Try
ConnectToServer(m_organiserEmailAddress)
ImpersonateUser(m_organiserEmailAddress)
Dim appointment As New Appointment(_service) With {
.Subject = eventCalendarItem.Subject}
'add attendees
For Each attendee In eventCalendarItem.Attendees
appointment.RequiredAttendees.Add(attendee.Email)
Next
'add categories
For Each category In eventCalendarItem.Categories
appointment.Categories.Add(Globals.GetEnumDescription(category))
Next
'add EventId = 5059 as an extended property of the appointment
appointment.SetExtendedProperty(m_eventIdExtendedPropertyDefinition, 5059)
appointment.Save(SendInvitationsMode.SendOnlyToAll)
Return appointment
Catch
Throw New Exception("Can't save appointment")
End Try
End Function
Private Shared Sub AcceptAppointmentAsAttendees(appointment As Appointment)
For Each attendee In appointment.RequiredAttendees
Try
ConnectToServer(attendee.Address.ToString())
ImpersonateUser(attendee.Address.ToString())
For Each a In FindRelatedAppiontments(appointment)
a.Categories.Add(Globals.GetEnumDescription(CalendarItemCategory.Workshop))
a.Update(ConflictResolutionMode.AlwaysOverwrite, SendInvitationsOrCancellationsMode.SendToNone)
a.Accept(True)
Next
Catch
Throw
End Try
Next
End Sub
Private Shared Function FindRelatedAppiontments(appointment As Appointment) As List(Of Appointment)
Dim view As New ItemView(1000)
Dim foundAppointments As New List(Of Appointment)
view.PropertySet =
New PropertySet(New PropertyDefinitionBase() {m_eventIdExtendedPropertyDefinition})
'Extended Property value = 5059
Dim searchFilter = New SearchFilter.IsEqualTo(m_eventIdExtendedPropertyDefinition, 5059)
For Each a In _service.FindItems(WellKnownFolderName.Calendar, searchFilter, view)
If a.ExtendedProperties.Count > 0 Then
foundAppointments.Add(appointment.Bind(_service, CType(a.Id, ItemId)))
End If
Next
Return foundAppointments
End Function
Well one thing is for sure that there is nothing straight forward in the EWS.
I will be honest with that, that till the moment, I did not work with integrating from the interior calendar to the Exchange calendar, my experience is the opposite , which is get what in the exchange to the internal one.
Anyway after reading your code, I think you are almost there. However I suggest that you catch the appointments that reach to the attendee by using the Streaming Notifications it is not that hard!
So I would say the steps should be like this
Create the appointment
Apply The extended property thing ( I suggest to use the GUID instead of hard coded number) as follows
Create an extended property, and put guid for the appointment, and it wont change unless you made a copy from another appointment (after all it is just a property)
private static readonly PropertyDefinitionBase AppointementIdPropertyDefinition = new ExtendedPropertyDefinition(DefaultExtendedPropertySet.PublicStrings, "AppointmentID", MapiPropertyType.String);
public static PropertySet PropertySet = new PropertySet(BasePropertySet.FirstClassProperties, AppointementIdPropertyDefinition);
//Setting the property for the appointment
public static void SetGuidForAppointement(Appointment appointment)
{
try
{
appointment.SetExtendedProperty((ExtendedPropertyDefinition)AppointementIdPropertyDefinition, Guid.NewGuid().ToString());
appointment.Update(ConflictResolutionMode.AlwaysOverwrite, SendInvitationsOrCancellationsMode.SendToNone);
}
catch (Exception ex)
{
// logging the exception
}
}
//Getting the property for the appointment
public static string GetGuidForAppointement(Appointment appointment)
{
var result = "";
try
{
appointment.Load(PropertySet);
foreach (var extendedProperty in appointment.ExtendedProperties)
{
if (extendedProperty.PropertyDefinition.Name == "AppointmentID")
{
result = extendedProperty.Value.ToString();
}
}
}
catch (Exception ex)
{
// logging the exception
}
return result;
}
Catch the appointment with the StreamingNotificationEvent. In my opinion a good way to do that is to run both Organizer and Attendee in the same time and catch the appointments between them.
To see an example, I have posted an answer to a previous question Multiple impersonation-threads in Exchange Web Service (EWS) . Please vote for the answers of both posts (here and there) if you found my answers are useful.
I do not want to make you scared, but once you solve your current problem; if you want to continue with it will get more complicated. I could write down how I solved the meetings problem, but I do not see it straight forward at all so it might be better if you write your own.
Cheers