set emacs indentation level
[lightning-exchange-provider:ianmartins-lightning-exchange-provider.git] / js / soapout.js
1 /* -*- espresso-indent-level: 8; -*- */
2 /* ***** BEGIN LICENSE BLOCK *****
3  * Version: AGPL 3.0
4  *
5  * The contents of this file may be used under the terms of the
6  * Affero GNU General Public License Version 3 or later (the "AGPL").
7  *
8  * ***** END LICENSE BLOCK ***** */
9
10 var xml_tag = '<?xml version="1.0" encoding="utf-8"?>\n';
11 var soap = new Namespace("soap", "http://schemas.xmlsoap.org/soap/envelope/");
12 var t = new Namespace("t", "http://schemas.microsoft.com/exchange/services/2006/types");
13 var m = new Namespace("m", "http://schemas.microsoft.com/exchange/services/2006/messages");
14
15 const fieldPathMap = {
16         'Body'                  : 'item',
17         'CalendarItemType'      : 'calendar',
18         'Categories'            : 'item',
19         'DateTimeReceived'      : 'item',
20         'DeletedOccurrences'    : 'calendar',
21         'End'                   : 'calendar',
22         'FirstOccurrence'       : 'calendar',
23         'IsAllDayEvent'         : 'calendar',
24         'IsCancelled'           : 'calendar',
25         'LastOccurrence'        : 'calendar',
26         'Location'              : 'calendar',
27         'MeetingTimeZone'       : 'calendar',
28         'ModifiedOccurrences'   : 'calendar',
29         'MyResponseType'        : 'calendar',
30         'OptionalAttendees'     : 'calendar',
31         'Organizer'             : 'calendar',
32         'Recurrence'            : 'calendar',
33         'RecurrenceId'          : 'calendar',
34         'ReminderDueBy'         : 'item',
35         'ReminderIsSet'         : 'item',
36         'ReminderMinutesBeforeStart' : 'item',
37         'RequiredAttendees'     : 'calendar',
38         'Start'                 : 'calendar',
39         'Subject'               : 'item',
40         'UID'                   : 'calendar',
41 };
42
43 const dayRevMap = {
44         'MO' : 'Monday',
45         'TU' : 'Tuesday',
46         'WE' : 'Wednesday',
47         'TH' : 'Thursday',
48         'FR' : 'Friday',
49         'SA' : 'Saturday',
50         'SU' : 'Sunday'
51 };
52
53 const dayIdxMap = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
54
55 const weekRevMap = {
56         '1' : 'First',
57         '2' : 'Second',
58         '3' : 'Third',
59         '4' : 'Fourth',
60         '-1': 'Last'
61 };
62
63 const monthIdxMap = ['January', 'February', 'March', 'April', 'May', 'June',
64                      'July', 'August', 'September', 'October', 'November', 'December'];
65
66
67 function makeAutodiscover(aEmail)
68 {
69         var req = <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006"/>;
70
71         req.Request.EMailAddress = aEmail;
72         req.Request.AcceptableResponseSchema = "http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a";
73
74         return xml_tag + String(req);
75 }
76
77 function makeSoapMessage(aReq)
78 {
79         var msg = <soap:Envelope xmlns:soap={soap} xmlns:t={t} xmlns:m={m}/>;
80
81         msg.soap::Header.t::RequestServerVersion.@Version = "Exchange2007_SP1";
82         msg.soap::Body.request = aReq;
83
84         return xml_tag + String(msg);
85 }
86
87 function makeFindItem(aItemFilter, aCount, aRangeStart, aRangeEnd)
88 {
89         var folder = "calendar";
90
91         var req = <m:FindItem xmlns:m={m} xmlns:t={t}/>;
92         req.@Traversal = "Shallow";
93
94         req.m::ItemShape.t::BaseShape = "IdOnly";
95         req.m::ItemShape.t::AdditionalProperties.content = <>
96             <t:FieldURI FieldURI="calendar:UID" xmlns:t={t}/>
97             <t:FieldURI FieldURI="calendar:CalendarItemType" xmlns:t={t}/>
98             </>;
99
100         // no ranges if searching for invitations XXX?
101         var wantInvitations = (aItemFilter & Components.interfaces.calICalendar.ITEM_FILTER_REQUEST_NEEDS_ACTION) != 0;
102         if (wantInvitations) {
103                 folder = "inbox";
104
105                 var restr =
106                         <m:Restriction xmlns:m={m} xmlns:t={t}>
107                                 <t:IsEqualTo>
108                                         <t:FieldURI FieldURI="item:ItemClass"/>
109                                         <t:FieldURIOrConstant>
110                                                 <t:Constant Value="IPM.Schedule.Meeting.Request"/>
111                                         </t:FieldURIOrConstant>
112                                 </t:IsEqualTo>
113                         </m:Restriction>;
114
115                 req.appendChild(restr);
116         } else if (aRangeStart && aRangeEnd) {
117                 var view = <m:CalendarView xmlns:m={m}/>;
118                 view.@StartDate = convDate(aRangeStart);
119                 view.@EndDate = convDate(aRangeEnd);
120
121                 if (aCount != 0) {
122                         view.@MaxEntriesReturned = aCount;
123                 }
124
125                 req.appendChild(view);
126         }
127
128         req.m::ParentFolderIds.t::DistinguishedFolderId.@Id = folder;
129
130         return makeSoapMessage(req);
131 }
132
133 function hex2b64(hexstr)
134 {
135         let binstr = '';
136
137         for each (let hexchar in hexstr.match(/../g)) {
138                 let binchar = String.fromCharCode('0x'+hexchar);
139                 binstr += binchar;
140         }
141
142         return binstr;
143 }
144
145 function makeGlobalIdRestriction(aUID)
146 {
147         /*
148          * We can not use FindItem with an UID directly.  Instead, we have to
149          * convert the UID to a GlobalObjectId, since the UID is generated from
150          * the GlobalObjectId.  There seem to be two different ways the UID is
151          * generated:
152          *
153          * 1) Same binary data: UID is encoded in hex, GID is in base64.
154          *    We just re-code the data.
155          *    This seems to be the case for all items created where Exchange
156          *    generates the GID itself.
157          *
158          * 2) Embedded arbitrary data: the UID is embedded in the GID.
159          *    We need to re-wrap the UID into the MAPI format that Exchange uses.
160          *    <http://msdn.microsoft.com/en-us/library/ee157690(v=EXCHG.80).aspx>
161          *    Not documented in this URL is that the unique data is the external
162          *    UID, NUL-terminated and prefixed with "vCal-Uid" and a
163          *    little endian 32 bit integer that always was 1 in my tests.
164          *    This is probably another counter to ensure uniqueness, but for now
165          *    we just assume for it to be 1.
166          *    The resulting string needs to be base64 encoded.
167          *
168          * We create a Restriction that matches (1) or (2), i.e. we search for both
169          * versions.
170          */
171         let gidprefix = '040000008200e00074c5b7101a82e0080000000000000000000000000000000000000000';
172         let vcaluid = 'vCal-Uid\1\0\0\0' + aUID + '\0';
173         let gid1 = btoa(hex2b64(aUID));
174
175         let gid2 = hex2b64(gidprefix);
176         for (let i = 0; i < 4; i++) {
177                 gid2 += String.fromCharCode((vcaluid.length >> (i * 8)) % 256);
178         }
179         gid2 += vcaluid;
180         gid2 = btoa(gid2);
181
182         let r1 = <t:IsEqualTo xmlns:t={t}>
183                         <t:ExtendedFieldURI DistinguishedPropertySetId="Meeting" PropertyId="3" PropertyType="Binary" xmlns:t={t}/>
184                         <t:FieldURIOrConstant>
185                                 <t:Constant Value={gid1}/>
186                         </t:FieldURIOrConstant>
187                 </t:IsEqualTo>;
188
189         let r2 = r1.copy();
190         r2..t::Constant.@Value = gid2;
191
192         let restr = <t:Or xmlns:t={t}/>;
193         restr.appendChild(r1);
194         restr.appendChild(r2);
195
196         return restr;
197 }
198
199 function makeFindItemUID(aUID)
200 {
201         var req = <m:FindItem xmlns:m={m} xmlns:t={t}/>;
202         req.@Traversal = "Shallow";
203
204         req.m::ItemShape.t::BaseShape = "IdOnly";
205         req.m::ItemShape.t::AdditionalProperties.content = <>
206             <t:FieldURI FieldURI="calendar:UID" xmlns:t={t}/>
207             <t:FieldURI FieldURI="calendar:CalendarItemType" xmlns:t={t}/>
208             </>;
209
210         req.m::Restriction.content = makeGlobalIdRestriction(aUID);
211
212         req.m::ParentFolderIds.t::DistinguishedFolderId.@Id = "calendar";
213
214         return makeSoapMessage(req);
215
216         // XXX missing: set organizer (use extended prop?)
217 }
218
219 function makeGetItemId(aItem)
220 {
221         var req = <m:GetItem xmlns:m={m} xmlns:t={t}/>;
222
223         req.m::ItemShape.t::BaseShape = 'IdOnly';
224         req.m::ItemShape.t::BodyType = 'Text';
225         req.m::ItemIds.t::ItemId.@Id = aItem.getProperty("X-ITEMID");
226
227         return makeSoapMessage(req);
228 }
229
230 function makeGetItem(aIds, aItemFilter)
231 {
232         var req = <m:GetItem xmlns:m={m} xmlns:t={t}/>;
233
234         req.m::ItemShape.t::BaseShape = 'IdOnly';
235         req.m::ItemShape.t::BodyType = 'Text';
236
237         var aprop = new XMLList();
238
239         for (var f in fieldPathMap) {
240                 var p = fieldPathMap[f] + ':' + f;
241
242                 aprop += <t:FieldURI FieldURI={p} xmlns:t={t}/>;
243         }
244
245         // snoozed until
246         aprop += <t:ExtendedFieldURI DistinguishedPropertySetId="Common" PropertyId="34144" PropertyType="SystemTime" xmlns:t={t}/>;
247
248         // global object id
249         aprop += <t:ExtendedFieldURI DistinguishedPropertySetId="Meeting" PropertyId="3" PropertyType="Binary" xmlns:t={t}/>;
250
251         req.m::ItemShape.t::AdditionalProperties.content = aprop;
252
253         var ritems = new XMLList();
254
255         var masters = {};
256         for each (var id in aIds) {
257                 var itid;
258
259                 switch (id.type) {
260                 case "Occurrence":
261                 case "Exception":
262                         /* collect every master only once, but also collect every exception */
263                         if (!masters[id.uid]) {
264                                 itid = <t:RecurringMasterItemId OccurrenceId={id.Id} xmlns:t={t}/>;
265                                 ritems += itid;
266                                 masters[id.uid] = true;
267                         }
268                         /* don't want Occurrences as items, only as master */
269                         if (id.type == "Occurrence") {
270                                 break;
271                         }
272                         /* FALLTHROUGH */
273                 default:
274                         itid = <t:ItemId Id={id.Id} xmlns:t={t}/>;
275                         ritems += itid;
276                         break;
277                 }
278         }
279
280         req.m::ItemIds.content = ritems;
281
282         return makeSoapMessage(req);
283 }
284
285 function makeTzSection(aType, aTz)
286 {
287         var r = <t:{aType} xmlns:t={t}/>;
288
289         var rrule = aTz.getFirstProperty("RRULE");
290
291         var m = aTz.getFirstProperty("TZOFFSETTO").value.match(/^([+-]?)(\d\d)(\d\d)$/);
292         var offs = cal.createDuration();
293         offs.hours = m[2];
294         offs.minutes = m[3];
295         if (m[1] == '+') {
296                 offs.isNegative = true;
297         }
298
299         m = aTz.getFirstProperty("TZOFFSETFROM").value.match(/^([+-]?)(\d\d)(\d\d)$/);
300         var offs_from = cal.createDuration();
301         offs_from.hours = m[2];
302         offs_from.minutes = m[3];
303         if (m[1] == '+') {
304                 offs_from.isNegative = true;
305         }
306
307         r.t::Offset = offs.icalString;
308
309         /* can't get parameters of RRULEs... have to do it manually :/ */
310         if (rrule) {
311             var prop = {};
312             for each (let ps in rrule.value.split(';')) {
313                     let m = ps.split('=');
314                     prop[m[0]] = m[1];
315             }
316
317             var m = prop["BYDAY"].match(/^(-?\d)(..)$/);
318             r.t::RelativeYearlyRecurrence.t::DaysOfWeek = dayRevMap[m[2]];
319             r.t::RelativeYearlyRecurrence.t::DayOfWeekIndex = weekRevMap[m[1]];
320             r.t::RelativeYearlyRecurrence.t::Month = monthIdxMap[prop["BYMONTH"] - 1];
321         }
322
323         let start = aTz.getFirstProperty("DTSTART").valueAsDatetime.clone();
324         start.addDuration(offs_from);
325         offs.isNegative = !offs.isNegative;
326         start.addDuration(offs);
327         r.t::Time = start.toString().substr(11, 8);
328
329         return (r);
330 }
331
332 function makeMeetingTimezone(aItem, e)
333 {
334         var tz = aItem.startDate.timezone;
335
336         if (tz.isUTC || tz.isFloating)
337                 return;
338
339         var mtz = <t:MeetingTimeZone xmlns:t={t}/>;
340         mtz.@TimeZoneName = tz.tzid;
341
342         var tzcomp = tz.icalComponent;
343
344         var dsttz = null;
345         for (var comp = tzcomp.getFirstSubcomponent("DAYLIGHT");
346              comp;
347              comp = tzcomp.getNextSubcomponent("DAYLIGHT")) {
348                 if (!dsttz || dsttz.getFirstProperty("DTSTART").valueAsDatetime.compare(
349                                 comp.getFirstProperty("DTSTART").valueAsDatetime) < 0) {
350                         dsttz = comp;
351                 }
352         }
353         var stdtz = null;
354         for (var comp = tzcomp.getFirstSubcomponent("STANDARD");
355              comp;
356              comp = tzcomp.getNextSubcomponent("STANDARD")) {
357                 if (!stdtz || stdtz.getFirstProperty("DTSTART").valueAsDatetime.compare(
358                                 comp.getFirstProperty("DTSTART").valueAsDatetime) < 0) {
359                         stdtz = comp;
360                 }
361         }
362
363         if (!stdtz) {
364                 return;
365         }
366
367         /* make the default offset zero */
368         mtz.t::BaseOffset = cal.createDuration().icalString;
369
370         mtz.appendChild(makeTzSection('Standard', stdtz));
371
372         if (!dsttz) {
373                 mtz.t::BaseOffset = mtz.t::Standard.t::Offset.toString();
374                 delete mtz.t::Standard;
375         } else {
376                 mtz.appendChild(makeTzSection('Daylight', dsttz));
377         }
378
379         e.appendChild(mtz);
380 }
381
382 function makeRecurrenceRule(aItem, e)
383 {
384         if (!aItem.recurrenceInfo || aItem.parentItem != aItem) {
385                 return;
386         }
387
388         var rrule = null;
389         for each (var ritem in aItem.recurrenceInfo.getRecurrenceItems({})) {
390                 if (calInstanceOf(ritem, Components.interfaces.calIRecurrenceRule)) {
391                         rrule = ritem;
392                         break;
393                 }
394         }
395
396         if (!rrule) {
397                 // XXX exception?
398                 return;
399         }
400
401         var r = <t:Recurrence xmlns:t={t}/>;
402
403         /* can't get parameters of RRULEs... have to do it manually :/ */
404         var prop = {};
405         for each (let ps in rrule.icalProperty.value.split(';')) {
406                 let m = ps.split('=');
407                 prop[m[0]] = m[1];
408         }
409
410         var startDate = aItem.startDate.clone();
411         startDate.isDate = true;
412
413         prop["BYMONTHDAY"] = prop["BYMONTHDAY"] || startDate.day;
414         prop["BYMONTH"] = prop["BYMONTH"] || (startDate.month + 1);
415
416         switch (rrule.type) {
417         case 'YEARLY':
418                 if (prop["BYDAY"]) {
419                         var m = prop["BYDAY"].match(/^(-?\d)(..)$/);
420                         r.t::RelativeYearlyRecurrence.t::DaysOfWeek = dayRevMap[m[2]];
421                         r.t::RelativeYearlyRecurrence.t::DayOfWeekIndex = weekRevMap[m[1]];
422                         r.t::RelativeYearlyRecurrence.t::Month = monthIdxMap[prop["BYMONTH"] - 1];
423                 } else {
424                         r.t::AbsoluteYearlyRecurrence.t::DayOfMonth = prop["BYMONTHDAY"];
425                         r.t::AbsoluteYearlyRecurrence.t::Month = monthIdxMap[prop["BYMONTH"] - 1];
426                 }
427                 break;
428         case 'MONTHLY':
429                 if (prop["BYDAY"]) {
430                         r.t::RelativeMonthlyRecurrence.t::Interval = rrule.interval;
431                         var m = prop["BYDAY"].match(/^(-?\d)(..)$/);
432                         r.t::RelativeMonthlyRecurrence.t::DaysOfWeek = dayRevMap[m[2]];
433                         r.t::RelativeMonthlyRecurrence.t::DayOfWeekIndex = weekRevMap[m[1]];
434                 } else {
435                         r.t::AbsoluteMonthlyRecurrence.t::Interval = rrule.interval;
436                         r.t::AbsoluteMonthlyRecurrence.t::DayOfMonth = prop["BYMONTHDAY"];
437                 }
438                 break;
439         case 'WEEKLY':
440                 r.t::WeeklyRecurrence.t::Interval = rrule.interval;
441                 var days = [];
442                 var daystr = prop["BYDAY"] || dayIdxMap[startDate.weekday];
443                 for each (let day in daystr.split(",")) {
444                         days.push(dayRevMap[day]);
445                 }
446                 r.t::WeeklyRecurrence.t::DaysOfWeek = days.join(' ');
447                 break;
448         case 'DAILY':
449                 r.t::DailyRecurrence.t::Interval = rrule.interval;
450                 break;
451         }
452
453         if (rrule.isByCount && rrule.count != -1) {
454                 r.t::NumberedRecurrence.t::StartDate = cal.toRFC3339(startDate);
455                 r.t::NumberedRecurrence.t::NumberOfOccurrences = rrule.count;
456         } else if (!rrule.isByCount && rrule.untilDate) {
457                 var endDate = rrule.untilDate.clone();
458                 endDate.isDate = true;
459                 r.t::EndDateRecurrence.t::StartDate = cal.toRFC3339(startDate);
460                 r.t::EndDateRecurrence.t::EndDate = cal.toRFC3339(endDate);
461         } else {
462                 r.t::NoEndRecurrence.t::StartDate = cal.toRFC3339(startDate);
463         }
464
465         /* We won't write WKST/FirstDayOfWeek for now because it is Exchange 2010 and up */
466
467         e.appendChild(r);
468 }
469
470 function makeDismissSnoozeState(aItem, e)
471 {
472         let par = aItem.parentItem;
473         let nextRem = cal.createDateTime("4501-01-01T00:00:00Z");
474
475         let lastAck = par.alarmLastAck;
476         let nextOcc;
477
478         if (par.recurrenceInfo) {
479                 if (!lastAck) {
480                         /* no ack yet, get first occurrence */
481                         nextOcc = par.recurrenceInfo.getOccurrences(par.startDate, null, 1, {})[0];
482                 } else {
483                         nextOcc = par.recurrenceInfo.getNextOccurrence(lastAck);
484                 }
485         } else {
486                 nextOcc = aItem;
487         }
488
489         let alarmDate = null;
490         let alarm = aItem.getAlarms({})[0];
491         if (nextOcc && alarm) {
492                 alarmDate = cal.alarms.calculateAlarmDate(nextOcc, alarm);
493         }
494
495         let lastSnooze = cal.createDateTime();
496         if (par.getProperty('X-MOZ-SNOOZE-TIME')) {
497                 lastSnooze = cal.createDateTime(par.getProperty('X-MOZ-SNOOZE-TIME'));
498         }
499
500         let props = aItem.propertyEnumerator;
501         while (props.hasMoreElements()) {
502                 let prop = props.getNext().QueryInterface(Components.interfaces.nsIProperty);
503                 if (prop.name.substr(0, 18) == 'X-MOZ-SNOOZE-TIME-') {
504                         let st = cal.createDateTime(prop.value);
505                         if (st.compare(lastSnooze) > 0) {
506                                 lastSnooze = st;
507                         }
508                 }
509         }
510
511         if (lastSnooze.compare(cal.createDateTime()) > 0 &&     /* valid snooze */
512             alarmDate &&                                        /* have upcoming alarm */
513             lastSnooze.compare(alarmDate) < 0 &&                /* snooze before alarm */
514             (!lastAck || lastSnooze.compare(lastAck) > 0)) {    /* snozze after ack */
515                 nextRem = lastSnooze;
516         } else if (alarmDate &&
517                    (!lastAck || alarmDate.compare(lastAck) > 0)) {
518                 nextRem = alarmDate;
519         }
520
521         nextRem = nextRem.getInTimezone(cal.UTC());
522
523         let eprop = <t:ExtendedProperty xmlns:t={t}/>;
524         eprop.t::ExtendedFieldURI.@DistinguishedPropertySetId = "Common";
525         eprop.t::ExtendedFieldURI.@PropertyId = "34144";
526         eprop.t::ExtendedFieldURI.@PropertyType = "SystemTime";
527         eprop.t::Value = cal.toRFC3339(nextRem);
528
529         e.appendChild(eprop);
530 }
531
532 function makeXmlItem(aItem)
533 {
534         var e = <t:CalendarItem xmlns:t={t} xmlns:m={m}/>;
535
536         e.t::Subject = aItem.title;
537         e.t::Body.@BodyType = "Text";
538         e.t::Body = aItem.getProperty('DESCRIPTION') || "";
539
540         var categories = aItem.getCategories({});
541         for each (var category in categories) {
542                 e.t::Categories.list += <t:String xmlns:t={t}>{category}</t:String>;
543         }
544
545         var alarms = aItem.getAlarms({});
546         if (alarms.length !== 0) {
547                 e.t::ReminderIsSet = 'true';
548
549                 // XXX only take the first for now
550                 let alarm = alarms[0];
551                 let alarmDate = cal.alarms.calculateAlarmDate(aItem, alarm);
552                 let offset = aItem.startDate.subtractDate(alarmDate);
553
554                 e.t::ReminderMinutesBeforeStart = offset.inSeconds / 60;
555         } else {
556                 e.t::ReminderIsSet = 'false';
557         }
558
559         makeDismissSnoozeState(aItem, e);
560
561         e.t::UID = aItem.id;
562
563         e.t::Start = convDate(aItem.startDate);
564         e.t::End = convDate(aItem.endDate);
565
566         e.t::IsAllDayEvent = aItem.startDate.isDate;
567
568         e.t::Location = aItem.getProperty("LOCATION") || "";
569
570         var attendees = aItem.getAttendees({});
571         for each (var attendee in attendees) {
572                 const attendeeStatus = {
573                         "NEEDS-ACTION"  : "Unknown",
574                         "TENTATIVE"     : "Tentative",
575                         "ACCEPTED"      : "Accept",
576                         "DECLINED"      : "Decline",
577                         null            : "Unknown",
578                 };
579
580                 var ae = <t:Attendee xmlns:t={t}/>;
581
582                 ae.t::Mailbox.t::Name = attendee.commonName;
583                 ae.t::Mailbox.t::EmailAddress = attendee.id.replace(/^mailto:/, '');
584                 ae.t::ResponseType = attendeeStatus[attendee.participationStatus];
585
586                 switch (attendee.role) {
587                 case "REQ-PARTICIPANT":
588                         e.t::RequiredAttendees.list += ae;
589                         break;
590                 case "OPT-PARTICIPANT":
591                         e.t::OptionalAttendees.list += ae;
592                         break;
593                 }
594         }
595
596         makeRecurrenceRule(aItem, e);
597         makeMeetingTimezone(aItem, e);
598
599         return e;
600 }
601
602 function makeCreateItem(aItem)
603 {
604         var req = <m:CreateItem xmlns:m={m} xmlns:t={t}/>;
605         req.@SendMeetingInvitations = sendMail(aItem);
606
607         req.m::SavedItemFolderId.t::DistinguishedFolderId.@Id = "calendar";
608
609         req.m::Items.content = makeXmlItem(aItem);
610
611         return makeSoapMessage(req);
612 }
613
614 function makeItemId(aItem)
615 {
616         var id;
617
618         /* handle trivial single/master case */
619         if (aItem.parentItem == aItem) {
620                 id = <t:ItemId xmlns:t={t}/>;
621                 id.@Id = aItem.getProperty("X-ITEMID");
622                 id.@ChangeKey = aItem.getProperty("SEQUENCE");
623                 return id;
624         }
625
626         var master = aItem.parentItem;
627
628         /* if we got here, we're dealing with an occurrence (exception) */
629         id = <t:OccurrenceItemId xmlns:t={t}/>;
630         id.@RecurringMasterId = master.getProperty("X-ITEMID");
631         id.@ChangeKey = master.getProperty("SEQUENCE");
632
633         /*
634          * EWS can not deal with RecurrenceIds, but only with
635          * numerical indices to identify an occurrence.
636          *
637          * To determine our index, we will have to enumerate all
638          * regular occurrences up to our original date and count them.
639          */
640
641         /* first find the recurrence rule */
642         var rrule = null;
643         for each (let ritem in master.recurrenceInfo.getRecurrenceItems({})) {
644                 if (calInstanceOf(ritem, Components.interfaces.calIRecurrenceRule)) {
645                         rrule = ritem;
646                         break;
647                 }
648         }
649
650         /*
651          * The end date needs to be after our original start date.  To be
652          * on the safe side, add the event duration.
653          */
654         var enddate = aItem.recurrenceId.clone();
655         enddate.addDuration(master.endDate.subtractDate(master.startDate));
656
657         var occurrences = rrule.getOccurrences(master.startDate,
658                                                master.startDate,
659                                                enddate,
660                                                0,
661                                                {});
662
663         id.@InstanceIndex = occurrences.length;
664
665         return id;
666 }
667
668 function getDeletedOccurrences(aItem)
669 {
670         var deleted = {};
671
672         if (!aItem.recurrenceInfo) {
673                 return deleted;
674         }
675
676         for each (let ritem in aItem.recurrenceInfo.getRecurrenceItems({})) {
677                 if (ritem.isNegative) {
678                         var oc = aItem.recurrenceInfo.getOccurrenceFor(ritem.date);
679                         var rid = cal.toRFC3339(oc.recurrenceId);
680                         deleted[rid] = oc;
681                 }
682         }
683
684         return deleted;
685 }
686
687 function checkDeletedOccurrences(aNewItem, aOldItem)
688 {
689         /*
690          * This function gets confused when the start date of the
691          * parent did change.  For now just assume that if the
692          * start date changed we won't delete occurrences at the same time.
693          */
694         if (aNewItem.startDate.compare(aOldItem.startDate) != 0) {
695                 return null;
696         }
697
698         var newlist = getDeletedOccurrences(aNewItem);
699         var oldlist = getDeletedOccurrences(aOldItem);
700
701         var dellist = [];
702         for (let del in newlist) {
703                 if (!oldlist[del]) {
704                         dellist.push(newlist[del]);
705                 }
706         }
707
708         if (dellist.length == 0) {
709                 return null;
710         }
711
712         var req = <m:DeleteItem xmlns:m={m}/>;
713         req.@DeleteType = "HardDelete";
714         req.@SendMeetingCancellations = sendMail(aOldItem);
715
716         var ids = new XMLList();
717         for each (let id in dellist) {
718                 ids += makeItemId(id);
719         }
720         req.m::ItemIds.content = ids;
721
722         return req;
723 }
724
725 function makeUpdateOneItem(aNewItem, aOldItem, aFlags)
726 {
727         var upd = <t:ItemChange xmlns:t={t}/>;
728         upd.id = makeItemId(aOldItem);
729
730         const noDelete = {
731                 'MeetingTimeZone'               : true,
732                 'ReminderMinutesBeforeStart'    : true
733         };
734
735         var ce = new XMLList();
736
737         var oe = makeXmlItem(aOldItem);
738         var ne = makeXmlItem(aNewItem);
739
740         for each (var prop in oe) {
741                 if (ne[prop.name()].length() > 0 || noDelete[prop.localName()]) {
742                         continue;
743                 }
744
745                 var de = <t:DeleteItemField xmlns:t={t}/>;
746                 if (prop.localName() == "ExtendedProperty") {
747                         de.t::ExtendedFieldURI = prop.t::ExtendedFieldURI;
748                 } else {
749                         de.t::FieldURI.@FieldURI = fieldPathMap[prop.localName()] + ':' + prop.localName();
750                 }
751
752                 ce += de;
753         }
754
755         for each (var prop in ne) {
756                 if (oe.children().contains(prop)) {
757                         continue;
758                 }
759
760                 var se = <t:SetItemField xmlns:t={t}/>;
761                 if (prop.localName() == "ExtendedProperty") {
762                         se.t::ExtendedFieldURI = prop.t::ExtendedFieldURI;
763                 } else {
764                         se.t::FieldURI.@FieldURI = fieldPathMap[prop.localName()] + ':' + prop.localName();
765                 }
766                 se.t::CalendarItem.content = prop;
767
768                 ce += se;
769         }
770
771         upd.t::Updates.content = ce;
772
773         if (!ce[0]) {
774                 return null;
775         }
776
777         /*
778          * If we change the recurrence, EWS will delete all exceptions.
779          * tell our caller that it will have to recreate them later.
780          */
781         if (se..t::Recurrence[0]) {
782                 for each (let ritem in aNewItem.recurrenceInfo.getRecurrenceItems({})) {
783                         if (cal.calInstanceOf(ritem, Components.interfaces.calIRecurrenceDate)) {
784                                 aFlags.createExceptions = true;
785                         }
786                 }
787         }
788
789         return upd;
790 }
791
792 function makeUpdateItem2(aItem, aUpdates)
793 {
794         var req = <m:UpdateItem xmlns:m={m} xmlns:t={t}/>;
795         req.@MessageDisposition = "SaveOnly";
796         req.@ConflictResolution = "AutoResolve";
797         req.@SendMeetingInvitationsOrCancellations = sendMail(aItem);
798
799         req.m::ItemChanges.content = aUpdates;
800
801         return req;
802 }
803
804 function makeUpdateItem(aNewItem, aOldItem, aFlags)
805 {
806         var dreq = checkDeletedOccurrences(aNewItem, aOldItem);
807         if (dreq) {
808                 return makeSoapMessage(dreq);
809         }
810
811         var upd = makeUpdateOneItem(aNewItem, aOldItem, aFlags);
812
813         if (!upd) {
814                 return "";
815         }
816
817         var req = makeUpdateItem2(aNewItem, upd);
818
819         return makeSoapMessage(req);
820 }
821
822 function makeDeleteItem(aItem)
823 {
824         var req = <m:DeleteItem xmlns:m={m}/>;
825         req.@DeleteType = "HardDelete";
826         req.@SendMeetingCancellations = sendMail(aItem);
827
828         req.m::ItemIds.id = makeItemId(aItem);
829
830         return makeSoapMessage(req);
831 }
832
833 function makeMeetingResponse(aItem, aResp)
834 {
835         var req = <m:CreateItem xmlns:m={m}/>;
836         req.@SendMeetingInvitations = sendMail(aItem);
837         req.@MessageDisposition = "SendAndSaveCopy";
838
839         const responseMap = {
840                 // XXX
841                 "NEEDS-ACTION"  : <t:TentativelyAcceptItem xmlns:t={t}/>,
842                 "TENTATIVE"     : <t:AcceptItem xmlns:t={t}/>,
843
844                 "ACCEPTED"      : <t:AcceptItem xmlns:t={t}/>,
845                 "DECLINED"      : <t:DeclineItem xmlns:t={t}/>
846         };
847
848         var r = responseMap[aResp];
849
850         if (!r) {
851                 return "";
852         }
853         r.t::ReferenceItemId.@Id = aItem.getProperty("X-ITEMID");
854         r.t::ReferenceItemId.@ChangeKey = aItem.getProperty("SEQUENCE");
855
856         req.m::Items.content = r;
857
858         return makeSoapMessage(req);
859 }
860
861 function makeGetUserAvailability(aId, aStart, aEnd)
862 {
863         var req = <m:GetUserAvailabilityRequest xmlns:m={m}/>;
864
865         /* WTF really?  Just give me UTC. */
866         req.t::TimeZone.t::Bias = 0;
867         req.t::TimeZone.t::StandardTime.t::Bias = 0;
868         req.t::TimeZone.t::StandardTime.t::Time = '00:00:00';
869         req.t::TimeZone.t::StandardTime.t::DayOrder = 1;
870         req.t::TimeZone.t::StandardTime.t::Month = 0;
871         req.t::TimeZone.t::StandardTime.t::DayOfWeek = "Sunday";
872         req.t::TimeZone.t::DaylightTime.content = req.t::TimeZone.t::StandardTime.children();
873
874         req.m::MailboxDataArray.t::MailboxData.t::Email.t::Address = aId;
875         req.m::MailboxDataArray.t::MailboxData.t::AttendeeType = 'Required';
876
877         req.t::FreeBusyViewOptions.t::TimeWindow.t::StartTime = convDate(aStart);
878         req.t::FreeBusyViewOptions.t::TimeWindow.t::EndTime = convDate(aEnd);
879
880         req.t::FreeBusyViewOptions.t::MergedFreeBusyIntervalInMinutes = 15;
881
882         return makeSoapMessage(req);
883 }