Updating an Appointment causes it to change to a Meeting in EWS 1.1 - exchange-server

Here's what I'm trying to do:
get all items on a user's calendar between two dates
update the Location or Subject for some items
I get the items with:
FindItemsResults<Appointment> findResults = calendar.FindAppointments(new CalendarView(startDate, endDate));
This query works fine. But whenever I call Update to save the item I get an exception:
Microsoft.Exchange.WebServices.Data.ServiceResponseException: One or more recipients are invalid.
Even though I get an exception, the item is saved and gets changed to have IsMeeting set to true! Now the updated item is a meeting with an organizer etc... This is, effectively, data corruption for me.
Here's the code. It is no more complicated than this. I've tested it by just changing Location or Subject and both cause the problem.
Appointment a = Appointment.Bind(_service, new ItemId(id));
a.Location = newLocation
a.Update(ConflictResolutionMode.AlwaysOverwrite);
Am I missing some concept or something? This seems like a pretty egregious problem.
FWIW, this is EWS 1.1 against an Office 365 server.

I figured it out with help from this Question:
Exchange Appointment Types
The key is the Update method needs to be called with the SendInvitationsOrCancellationsMode.SendToNone flag set in the 2nd parameter.
Like this:
a.Update(ConflictResolutionMode.AlwaysOverwrite, SendInvitationsOrCancellationsMode.SendToNone);

So tig's answer works when you never want to send out appointment updates to the other attendees. However to answer this properly you actually need to get the attendee state loaded.
By default it is trying to send appointment updates to the attendees, however your appointment object doesn't have the attendee state loaded and is hence blowing up. When you do the bind you should load the attendee properties. You should probably also load the organizer as well to cover another edge case:
AppointmentSchema.RequiredAttendees
AppointmentSchema.OptionalAttendees
AppointmentSchema.Resources
AppointmentSchema.Organizer
This will get the attendees populated if you want to do an update that sends out updates to the attendees.
However there is then another edge case that you have to worry about. If you have an appointment with no attendees added to it (just the organizer), then EWS may still complain and throw this error. It will actually work for appointments in some states, but fail in other states.
So the most complete solution is a combination of:
Loading the attendee state.
Inspecting the attendee state to see if there are any attendees other than the organizer (depending on how the appointment was created the organizer may or may not appear in the RequiredAttendees collection). If there are not then you must use SendInvitationsOrCancellationsMode.SendToNone.
So the full sample would look something like:
Appointment a = Appointment.Bind(_service, new ItemId(id), new PropertySet(AppointmentSchema.RequiredAttendees, AppointmentSchema.OptionalAttendees, AppointmentSchema.Resources, AppointmentSchema.Organizer));
a.Location = newLocation
// Check if the appointment has attendees other than the organizer. The organizer may
// or may not appear in the required attendees list.
if (HasNoOtherAttendee(a.Organizer.Address, a.RequiredAttendees) &&
(a.OptionalAttendees.Count == 0) && (a.Resources.Count == 0))
{
a.Update(ConflictResolutionMode.AlwaysOverwrite, SendInvitationsOrCancellationsMode.SendToNone);
}
else
{
// We have other attendees in the appointment, so we can use SendToAllAndSaveCopy so
// they will see the update.
a.Update(ConflictResolutionMode.AlwaysOverwrite, SendInvitationsOrCancellationsMode.SendToAllAndSaveCopy);
}
bool HasNoOtherAttendee(string email, AttendeeCollection attendees)
{
bool emptyOrOnlyMe = true;
foreach (var a in attendees)
{
if (!string.Equals(email, a.Address, StringComparison.OrdinalIgnoreCase))
{
emptyOrOnlyMe = false;
break;
}
}
return emptyOrOnlyMe;
}

To answer this bit of the question
"Even though I get an exception, the item is saved and gets changed to
have IsMeeting set to true! Now the updated item is a meeting with an
organizer etc... This is, effectively, data corruption for me."
The Microsoft documentation states, in the small print, "A meeting request is just an appointment that has attendees. You can convert an appointment into a meeting request by adding required attendees, optional attendees, or resources to the appointment" - as seen here
http://msdn.microsoft.com/en-us/library/office/dd633641%28v=exchg.80%29.aspx
In other words, as soon as you have any attendees, Exchange converts it to a meeting automatically.

public static bool UpdateAppointment(ExchangeCredential credentials,
ItemId appointmentId, string newLocation, string newSubject,
DateTime startTime,
DateTime endTime)
{
ExchangeService service = GetExchangeService(credentials);
try
{
Appointment appt = Appointment.Bind(service, appointmentId,
new PropertySet(BasePropertySet.IdOnly, AppointmentSchema.Start,
AppointmentSchema.ReminderDueBy, AppointmentSchema.End, AppointmentSchema.StartTimeZone,
AppointmentSchema.TimeZone));
appt.Location = newLocation;
appt.Start = startTime;
appt.End = endTime;
appt.Subject = newSubject;
// very important! you must load the new timeZone
appt.StartTimeZone = TimeZoneInfo.Local;
//appt.Body.Text = newBody; //if needed
appt.Update(ConflictResolutionMode.AlwaysOverwrite);
}
catch (Exception ex)
{
throw ex;
}
return true;
}

Related

Field Service: correct way to cancel a BookableResourceBooking via SDK/API?

The BookableResourceBooking entity is documented here:
https://learn.microsoft.com/en-us/dynamics365/customerengagement/on-premises/developer/entities/bookableresourcebooking
I'd like to able to cancel a booking but I can't seem to find any SDK or API docs that explain how to do so. Would changing the bookingstatus value to "canceled" be sufficient to cancel a booking? Where would I input the reason code?
You can do this using SDK as well as API.
When you see Bookable Resource Booking in CRM, you can see Deactivate button. Clicking on it will deactivte the Bookable Resource Booking.
Now there is one more clean way to manage data, you can set Booking status to cancelled and then deactivate record in this way you can capture complete data as why Bookable Resource Booking record is cancelled/deactivated.
var entity = {};
entity["bookingstatus#odata.bind"] = "/bookingstatuses(bbda588b-013a-eb11-a813-000d3a25bbe9)"; /* cancelled booking status ID*/
entity.statecode = 1; /*Inactive*/
entity.statuscode = 2; /*Inactive*/
Xrm.WebApi.online.updateRecord("bookableresourcebooking", "bbda588b-013a-eb11-a813-000d3a25bbe9", entity).then(
function success(result) {
var updatedEntityId = result.id;
},
function(error) {
Xrm.Utility.alertDialog(error.message);
}
);

GoogleCalendarAPI accept/decline event

I am working on GoogleCalendar API and using node.js as a platform to build my application.
I am able to create events using authentication procedure and creating the calendar event using the access token generated while authenticating.
My question is that suppose if we have any attendee in the event and I want to accept/decline the event using the calendar API from the attendee's side, how can we do that?
I have tried fetching the calendar event of the attendee and matching it with the iCalUID of the event which was originally created and then modifying the event using update event on the attendee's calendar.
Event creator or owner cannot modify the response of attendees. Only attendees can modify their status.
To update the status on the side of the user, You may use the Event.update API and provide value for 'attendees.responseStatus'. Attendee's response status has 4 (four) possible value (described below).
'needsAction' - has not responded to the invitation.
'declined' - has declined the invitation.
'tentative' - has tentatively accepted the invitation
'accepted' - has accepted the invitation.
In addition to this, You can use the word "primary" as value for the calendar id to represent the currently logged in user
CalendarId: Calendar identifier. To retrieve calendar IDs call the calendarList.list method. If you want to access the primary calendar of the currently logged in user, use the "primary" keyword. (string).
For the id, you need to use the "id" returned by the Events.list API not the "iCalUID". Those two are different as described here.
Other fields that you need to provide are the email (of the attendee), startdate and enddate.
For more information, you may view the official documentation, link below:
https://developers.google.com/google-apps/calendar/v3/reference/events
Here is an example in java, using PATCH. Create an event object with the just the information you want to change, in this case the attendee and the response status. This code is running as the attendee.
final Event event = new Event()
.setAttendees(Arrays.asList(new EventAttendee().setEmail(email)
.setResponseStatus("declined")));
try
getCalendarService(googleAccountCredential).events()
.patch(CALENDAR_PRIMARY, calendarEventId, event)
.setSendNotifications(true)
.setOauthToken(googleAccountCredential.getToken()).execute();
return true;
} catch (final Exception ex) {
...
return false;
}
}
Like Android Enthusiast discussed, only the attendee can update his or her calendar from the attendee's side. You should also check the documentation as he suggested. The answer below is a working example for node.js and python
To update the event, you need to have the eventId and the user email. Get the event from the calendar(with the eventID),
loop through all the attendees, change responseStatus for
that particular attendee and then update the google calendar
For node js using the google api
const { google } = require('googleapis');
const calendar = google.calendar({ version: 'v3', auth: 'YOUR-API-KEY-HERE' });
#get the event to be updated
let theEvent = calendar.events.get({ calendarId: 'primary', eventId: eventId })
#loop through the whole attendee
for (let i = 0, i < theEvent['atendees'].length; i++){
if (theEvent['atendees'][i]['email'] === userEmail){
theEvent['atendees'][i]['responseStatus'] = 'accepted'
}
}
#update the google event
calendar.events.update({ calendarId: 'primary', eventId: theEventId, body: theEvent}, function(err, event) {
  if (err) {
    console.log('there was an error');
    return;
  }
  console.log('Event updated');
});
For python using googleapiclient
from googleapiclient.discovery import build
calendar = build('calendar', 'v3', credentials=credential)
event = calendar.events().get(calendarId='primary', eventId='eventId').execute()
For attendee in event['atendees']:
if atendee['email'] == user_email:
attendee['responseStatus'] = 'accepted'
break
#update the google event
udated_event = calendar.events().update(calendarId='primary', eventId=eventId, body=event).execute()
Lets suppose that you already have the event payload with the attendees key, then you need to get the ID for the created event:
created_event = gcal_service.events().insert(
calendarId='primary', body=event_payload
).execute()
then copy the attendees in a new object
accepted_attendees = {}
accepted_attendees['attendees'] = event_payload['attendees'].copy()
and now what you need to do is submit a patch to the attendees calendar based on the event_id, like this:
for attendee in event_payload['attendees']:
attendee_pos = accepted_attendees['attendees'].index(attendee)
accepted_attendees['attendees'].pop(attendee_pos)
accepted_attendees['attendees'].append({
'email': attendee['email'],
'self': True,
'responseStatus': 'accepted',
'additionalGuests': 0,
})
gcal_service.events().patch(
calendarId='primary',
eventId=created_event['id'],
body=accepted_attendees
)
And that's all, all the other attendees, now have accepted the event, hope it helps.
To respond, you need to get the event with the same event id from the attendee's calendar and then perform a patch or an update operation changing the response status of this attendee from needsAction to accepted / declined.
A bit of documentation on how events are copied between attendees and organizers:
https://developers.google.com/google-apps/calendar/concepts/sharing
Here is an example in python for Google Calendar Api v3. You can either use update or patch. Both of them are working.
all_attendees = event['attendees']
event['attendees'] = [{
'email': 'you#example.com',
'self': True,
'responseStatus': 'accepted',
'additionalGuests': 0,
}]
updated_event = service.events().patch(calendarId=calendar_id, eventId=event_id, body=event).execute()
Have fun

How can I determine the revision of a calendar item in EWS when using a PullSubscription

I am trying to do some synchronization work with and Exchange Calendar. I want to keep another external calendar in sync with Exchange. Currently when the other app triggers a creation or update of some sort in Exchange, that change is then sent back to the other calendar creating an endless loop.
I had hoped to use the AppointmentSequenceNumber property when binding the Appointment item, but it always has the value of 0 no matter how many times it is updated. I am including AppointmentSequenceNumber in my PropertySet.
If anyone knows of a way to catch these updates and keep them from being sent back, that would be very helpful.
Thank you.
PropertySet pSet = new PropertySet(BasePropertySet.FirstClassProperties, ItemSchema.Subject, ItemSchema.Body, AppointmentSchema.Start, AppointmentSchema.End,AppointmentSchema.ArchiveTag, AppointmentSchema.InstanceKey, AppointmentSchema.AppointmentSequenceNumber);
ChangeCollection<ItemChange> changes = null;
.....
ExchangeService service = new ExchangeService(ExchangeVersion.Exchange2013)
{
Url = new Uri(exInfo.ServiceURL),
Credentials = new WebCredentials(exInfo.UserName, exInfo.Password)
};
//Pull Subscription Info
Microsoft.Exchange.WebServices.Data.PullSubscription sub = service.SubscribeToPullNotifications(
new FolderId[] { WellKnownFolderName.Calendar }, 30, "",
EventType.Created, EventType.Modified, EventType.Deleted);
syncState = exInfo.SyncState;
//Pull Changes
while (!syncComplete )//&& Count < MaxItems)
{
changes = service.SyncFolderItems(new FolderId(WellKnownFolderName.Calendar),
PropertySet.FirstClassProperties, null, 100, SyncFolderItemsScope.NormalItems, syncState);
foreach (ItemChange change in changes)
{
if (change.ChangeType != ChangeType.Delete) { eventItem = Appointment.Bind(service, change.ItemId, pSet); }
switch (change.ChangeType)
{
case ChangeType.Update:
...
break;
case ChangeType.Create:
...
break;
case ChangeType.Delete:
...
break;
}
Count++;
}
syncState = changes.SyncState;
syncComplete = !changes.MoreChangesAvailable;
}...
The AppointmentSequenceNumber would only be valid for Meetings; on normal Appointments it isn't used.
I had hoped to use the AppointmentSequenceNumber property when binding the Appointment item
That wouldn't work even if it was incrementing. Exchange will always provide you with the current version and the only things valid in a Bind is the EWSId of the appointment (or the Recurrence Sequence).
If anyone knows of a way to catch these updates and keep them from being sent back, that would be very helpful.
Synchronization is complicated but (from a notification perspective) if you modify an item in Exchange it's going to fire a notification and the ChangeKey attribute on the Item will be updated (quote):
"When you work with items in Exchange, another value to keep in mind is the ChangeKey attribute. This value, in addition to the item ID, is used to keep track of the state of an item. Any time an item is changed, a new change key is generated. When you perform an UpdateItem operation, for example, you can use the ChangeKey attribute to let the server know that your update is being applied to the most current version of the item. If another application made a change to the item you’re updating, the change keys won’t match and you will not be able to perform the update."

Stuck for over 2 months getting an outlook addin to work

This is basically a re-post of a previous question, but I've spent over 2 months stuck on this same issue and I haven't made any progress of any kind. Long story short, sometimes it fires and sometimes it doesn't. Sometimes it loads once, sometimes Outlook defaults it to "inactive" and there's nothing I seem to be able to do about it. When it DOES fire, it hangs up when trying to send the first email. So, I have old appointments outside of the date range I'm checking and the messagebox appears for those. When it gets to "new" appointments (within the date range), sometimes it pops up the first messagebox but hangs up trying to send the email. SOmetimes that first "good" messagebox fails to pop up. The last advice I got regarding this issue was to build a log file, but I couldn't figure out how/what good it was going to do me or honestly I wasn't even sure what I was going to need to log, and the gentleman who suggested it never responded to me when I asked. Thank you in advance for your help, this is easily one of the most frustrating things I've ever run in to as a developer.
using System;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Outlook = Microsoft.Office.Interop.Outlook;
using Office = Microsoft.Office.Core;
using Microsoft.Office.Interop.Outlook;
using System.Windows.Forms;
namespace OutlookAddIn1
{
public partial class ThisAddIn
{
//Outlook.Inspectors inspectors;
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
doStuff();
}
private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
{
}
#region VSTO generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InternalStartup()
{
this.Startup += new System.EventHandler(ThisAddIn_Startup);
this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
}
//https://msdn.microsoft.com/en-us/library/ms268866.aspx
private void doStuff()
{
Outlook.Application app = new Outlook.Application();
Thread.Sleep(30000); //120 seconds - was 120000
DateTime firstRun = DateTime.Now; //So we can check every 24 hours? Maybe once initially as well.
DateTime lastRun = DateTime.Now;//.AddHours(1); //We're going to compare this to firstRun
bool whileTrue = true;
//int test = 0;
try
{
while (whileTrue)
{
if (whileTrue == true)//(firstRun > lastRun.AddDays(1))
{
Outlook.MAPIFolder calendarFolder = Application.Session.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderCalendar);
Outlook.Items outlookCalendarItems = calendarFolder.Items;
outlookCalendarItems.IncludeRecurrences = true; //was true
List<Outlook.AppointmentItem> lst = new List<Outlook.AppointmentItem>();
foreach (Outlook.AppointmentItem item in outlookCalendarItems)
{
lst.Add(item);
//We can probably just handle logic in here without the second for loop that comes next
}
foreach (Outlook.AppointmentItem x in lst)
{
DateTime startDate = DateTime.Now.AddDays(1);
DateTime endDate = DateTime.Now.AddDays(5);
DateTime apptDate = x.Start;
if (x.Subject.ToLower().Contains("telos"))
{
MessageBox.Show("X: " + x.Start + "XYZ: " + x.Subject);
if (x.Start > startDate && x.Start < endDate)
{
Microsoft.Office.Interop.Outlook.MailItem email = app.CreateItem((OlItemType.olMailItem));
//Outlook.MailItem mail = (Outlook.MailItem)Globals.ThisAddIn.Application.CreateItem(Outlook.OlItemType.olMailItem);
//Outlook.Recipient recipient = Globals.ThisAddIn.Application.Session.CreateRecipient("cindy#soundstewardship.com");
//email.Sender = recipient.AddressEntry;
//Outlook.Recipient recipient = app.Session.CreateRecipient("someone#example.com");
//email.Sender = recipient.AddressEntry;
//email.SentOnBehalfOfName = "someone#example.com";
email.Display(true); //was false
email.Subject = "You have a new appointment";
email.Importance = Outlook.OlImportance.olImportanceLow;
email.To = Application.Session.CurrentUser.AddressEntry.Address; //Current email address.
email.Body = "This email was automatically generated to remind you have an upcoming appointment on: " + x.Start.ToString();
email.Save();
email.Close(OlInspectorClose.olSave);
//((Outlook._MailItem)email).Send();
//email.Send();
//((Outlook._MailItem)mailItem).Send();
}
}
}
lastRun = DateTime.Now;
whileTrue = false;
}
else
{
/*
Outlook.MailItem email = new Outlook.MailItem();
email.Subject = "This is only a test.";
email.To = Application.Session.CurrentUser.AddressEntry.Address; //Current email address.
email.Body = "This is only a test.";
//email.Send();
((Outlook._MailItem)email).Send();
* */
}
}
}
catch (System.Exception e) //Microsoft.Office.Interop.Outlook.Exception e
{
MessageBox.Show(e.InnerException.ToString());
}
finally
{
app.Quit();
}
}
#endregion
}
}
First of all, there is no need to create a new Outlook Application instance in the code. You need to use the Application property of the add-in class.
At startup, I need it to read all appointments whose subject contains a certain string
Don't use the foreach for iterating over all items in the folder. Instead, you need to use the Find/FindNext or Restrict methods of the Items class. You may read more about these methods in the following articles (the sample code is included):
How To: Retrieve Outlook calendar items using Find and FindNext methods
How To: Use Restrict method in Outlook to get calendar items
When you are done I'd recommend using the Resolve or ResolveAll methods of the Recipient(s) class to resolve all recipients against the address book.
Also, like 75% of the time this addin loads directly as "inactive" and doesn't fire.
Microsoft Office applications can disable add-ins that behave unexpectedly. If an application does not load your add-in, the application might have hard disabled or soft disabled your add-in.
Hard disabling can occur when an add-in causes the application to close unexpectedly. It might also occur on your development computer if you stop the debugger while the Startup event handler in your add-in is executing.
Soft disabling can occur when an add-in produces an error that does not cause the application to unexpectedly close. For example, an application might soft disable an add-in if it throws an unhandled exception while the Startup event handler is executing.When you re-enable a soft-disabled add-in, the application immediately attempts to load the add-in. If the problem that initially caused the application to soft disable the add-in has not been fixed, the application will soft disable the add-in again. Read more about that in the How to: Re-enable an Add-in That Has Been Disabled article.
Also Outlook 2013 monitors add-in performance metrics such as add-in startup, shutdown, folder switch, item open, and invoke frequency. Outlook records the elapsed time in milliseconds for each performance monitoring metric. For example, the startup metric measures the time required by each connected add-in during Outlook startup. Outlook then computes the median startup time over 5 successive iterations. If the median startup time exceeds 1000 milliseconds (1 second), then Outlook disables the add-in and displays a notification to the user that an add-in has been disabled. The user has the option of always enabling the add-in, in which case Outlook will not disable the add-in even if the add-in exceeds the 1000 millisecond performance threshold. See Performance criteria for keeping add-ins enabled for more information.
Why would you sleep on the main Outlook thread? And then loop through all items in the folder instead of using Items.Restrict or Items.Find/FindNext?

How to save a record and immediately use its GUID

I'm executing some javascript from a ribbon button and what I want to do is save the record that I am creating and then immediately use its GUID for some code a bit further on. Each time I try it the GUID is coming back null even though I'm requesting it after the record has been saved. If I try the button again after I've saved it then it works, but not as I'm saving it.
Is there a way to do this?
function RibbonButton_AddProduct()
{
//Save the Record
Xrm.Page.data.entity.save();
LoadProductCreate();
}
function LoadProductCreate()
{
var serverUrl;
var errorMessage = "Context to retrieve the Server URL is not available.";
if (typeof GetGlobalContext != "undefined"){
serverUrl = GetGlobalContext().getServerUrl();
} else {
if (typeof Xrm != "undefined"){
serverUrl = Xrm.Page.context.getServerUrl();
} else {
alert(errorMessage);
return;
}
}
if (serverUrl.match(/\/$/)){
serverUrl = serverUrl.substring(0, serverUrl.length - 1);
}
var recordId = Xrm.Page.data.entity.getId();
alert(recordId);
var url = serverUrl + "/main.aspx?etc=10030&extraqs=%3f_CreateFromId%3d%"+recordId
+"%257d%26_CreateFromType%3d10029%26etc%3d10030%26"
+"pagemode%3diframe%26preloadcache%3d1345465354543&pagetype=entityrecord";
window.open(url);
}
Here’s a different approach to solving this problem.
What you are trying to do is ‘working against the system’ - you are effectively making two save buttons. In the rest of Crm when the Id is required for a ribbon button the record must first be saved. E.g. you can’t use the dialog or workflow buttons on an unsaved record, you also can’t 'add new/existing' to an unsaved record.
So my solution would be to disable the button on unsaved forms, force the user to save the record manually and then allow them to use your button - this is the way Crm is meant to be used, and is the way the rest of Crm will work.
You should not work against the system, you should work with it, you have a product to customise and extend – not change.
If this doesn’t meet your requirement I would suggest uses Greg’s suggestion (1) of having flags, though it sounds a bit messy - but then this is a requirement that inherently is.
You could try one of two things:
Add a hidden boolean attribute to your form(e.g. "new_launchProductCreate"), set it in code prior to save and then read it onLoad.
Instead of setting the value prior to create (and therefore potentially commiting it to the database), you could create a plugin registered against the "Create" step of your record that injects a boolean value into the Entity.Attributes collection as the record is returned to the user. This would prevent the value persisting into the database and running every time your form loads.
You can instead use AJAX to reset the value as you launch your onLoad code so that it doesn't trigger on every form load
Assign the record guid manually, use AJAX to save your record, pop your new window using th enew guid and then reload your original form (so that the form is no longer in an "unsaved" state).
At the risk of being proven wrong as I cannot verify this right away... you will need to save and then reload the page.
The value stored in Xrm.Page.data.entity.getId() is set when the page is loaded/initialised and hence won't be updated when you access it after you have called Save().
It is also why it does work when you reload the page.
Perhaps you could call save and then reload the window adding a querystring variable of your own, to indicate that this event has just occurred?
e.g.
function DoSomething() {
//do your stuff
Xrm.Page.data.entity.save();
//something like - sure someone can do better!
window.location = window.location.href + '&foo=bar';
}
and then register something like this onFormLoad
function OnLoad() {
var queryStringParms = Xrm.Page.context.getQueryStringParameters();
//test to see if your query string param exists here
for (var i in queryStringParams) {
//if you find query string, do extra processing here
}
}

Resources