fix erroneous alarms for exceptions of recurring items
[lightning-exchange-provider:ianmartins-lightning-exchange-provider.git] / js / soapin.js
1 /* ***** BEGIN LICENSE BLOCK *****
2  * Version: AGPL 3.0
3  *
4  * The contents of this file may be used under the terms of the
5  * Affero GNU General Public License Version 3 or later (the "AGPL").
6  *
7  * ***** END LICENSE BLOCK ***** */
8
9 var soap = new Namespace("soap", "http://schemas.xmlsoap.org/soap/envelope/");
10 var t = new Namespace("t", "http://schemas.microsoft.com/exchange/services/2006/types");
11 var m = new Namespace("m", "http://schemas.microsoft.com/exchange/services/2006/messages");
12
13 const participationMap = {
14         "Unknown"       : "NEEDS-ACTION",
15         "Tentative"     : "TENTATIVE",
16         "Accept"        : "ACCEPTED",
17         "Decline"       : "DECLINED",
18         "Organizer"     : "ACCEPTED"
19 };
20
21 const dayMap = {
22         'Monday'        : 'MO',
23         'Tuesday'       : 'TU',
24         'Wednesday'     : 'WE',
25         'Thursday'      : 'TH',
26         'Friday'        : 'FR',
27         'Saturday'      : 'SA',
28         'Sunday'        : 'SU',
29         'Weekday'       : ['MO', 'TU', 'WE', 'TH', 'FR'],
30         'WeekendDay'    : ['SA', 'SO'],
31         'Day'           : ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SO']
32 };
33
34 const weekMap = {
35         'First'         : 1,
36         'Second'        : 2,
37         'Third'         : 3,
38         'Fourth'        : 4,
39         'Last'          : -1
40 };
41
42 const monthMap = {
43         'January'       : 1,
44         'February'      : 2,
45         'March'         : 3,
46         'April'         : 4,
47         'May'           : 5,
48         'June'          : 6,
49         'July'          : 7,
50         'August'        : 8,
51         'September'     : 9,
52         'October'       : 10,
53         'November'      : 11,
54         'December'      : 12
55 };
56
57
58 function readRecurrenceRule(recit)
59 {
60         /*
61          * The Mozilla recurrence API is bothersome and dictated by libical.
62          *
63          * We need to obey and build an iCalendar string which we feed in
64          * to get the proper recurrence info.
65          */
66
67         var comps = {};
68
69         for each (var rec in recit) {
70                 switch (rec.localName()) {
71                 case "RelativeYearlyRecurrence":
72                 case "AbsoluteYearlyRecurrence":
73                         comps['FREQ'] = "YEARLY";
74                         break;
75                 case "RelativeMonthlyRecurrence":
76                 case "AbsoluteMonthlyRecurrence":
77                         comps['FREQ'] = "MONTHLY";
78                         break;
79                 case "WeeklyRecurrence":
80                         comps['FREQ'] = "WEEKLY";
81                         break;
82                 case "DailyRecurrence":
83                         comps['FREQ'] = "DAILY";
84                         break;
85                 case "NoEndRecurrence":
86                 case "EndDateRecurrence":
87                 case "NumberedRecurrence":
88                         break;
89                 default:
90                         sdbg("skipping " + rec.localName() + "\n");
91                         continue;
92                 }
93
94                 var weekdays = [];
95                 var week = [];
96                 for each (var comp in rec) {
97                         switch (comp.localName()) {
98                         case 'DaysOfWeek':
99                                 for each (let day in comp.toString().split(" ")) {
100                                         weekdays = weekdays.concat(dayMap[day]);
101                                 }
102                                 break;
103                         case 'DayOfWeekIndex':
104                                 week = weekMap[comp.toString()];
105                                 break;
106                         case 'Month':
107                                 comps['BYMONTH'] = monthMap[comp.toString()];
108                                 break;
109                         case 'DayOfMonth':
110                                 comps['BYMONTHDAY'] = comp.toString();
111                                 break;
112                         case 'FirstDayOfWeek':
113                                 comps['WKST'] = dayMap[comp.toString()];
114                                 break;
115                         case 'Interval':
116                                 comps['INTERVAL'] = comp.toString();
117                                 break;
118                         case 'StartDate':
119                                 /* Dunno what to do with this; no place to set */
120                                 break;
121                         case 'EndDate':
122                                 comps['UNTIL'] = comp.toString().replace(/Z$/, '');
123                                 break;
124                         case 'NumberOfOccurrences':
125                                 comps['COUNT'] = comp.toString();
126                                 break;
127                         }
128                 }
129
130                 let wdtemp = weekdays;
131                 weekdays = [];
132                 for each (let day in wdtemp) {
133                         weekdays.push(week + day);
134                 }
135                 if (weekdays.length > 0) {
136                         comps['BYDAY'] = weekdays.join(',');
137                 }
138         }
139
140         var compstrs = [];
141         for (var k in comps) {
142                 compstrs.push(k + "=" + comps[k]);
143         }
144
145         if (compstrs.length == 0) {
146                 return null;
147         }
148
149         var recrule = cal.createRecurrenceRule();
150
151         /* need to do this re-assign game so that the string gets parsed */
152         var prop = recrule.icalProperty;
153         prop.value = compstrs.join(';');
154         recrule.icalProperty = prop;
155
156         return recrule;
157 }
158
159 function readRecurrence(cit, it)
160 {
161         var recrule = readRecurrenceRule(it.t::Recurrence.children());
162
163         if (recrule === null) {
164                 return null;
165         }
166
167         var recurrenceInfo = cal.createRecurrenceInfo(cit);
168         recurrenceInfo.setRecurrenceItems(1, [recrule]);
169
170         return recurrenceInfo;
171 }
172
173 function readModifiedOccurrences(cit, it, exitems)
174 {
175         for each (var mit in it.t::ModifiedOccurrences.children()) {
176                 var key = [mit.t::ItemId.@Id, mit.t::ItemId.@ChangeKey];
177                 var oit = exitems[key];
178
179                 if (!oit || !cit.recurrenceInfo) {
180                         continue;
181                 }
182                 /*
183                  * We need to clear alarmLastAck, because it has been
184                  * read before and might contain some value we need
185                  * to ignore.  All alarm dismiss/snooze is handled
186                  * in the parent item.
187                  */
188                 oit.alarmLastAck = null;
189                 cit.recurrenceInfo.modifyException(oit, true);
190                 delete exitems[key];
191         }
192 }
193
194 function readDeletedOccurrences(cit, it)
195 {
196         for each (var mit in it.t::DeletedOccurrences.children()) {
197                 cit.recurrenceInfo.removeOccurrenceAt(cal.fromRFC3339(mit.t::Start, cal.UTC()));
198         }
199 }
200
201 function formatTZoff(off)
202 {
203         var s = "";
204
205         /*
206          * For some very special reason EWA and RFC5545 use different
207          * opinions on what "UTC offset" and "offset from UTC" means.
208          */
209         if (off.isNegative) {
210                 s += "+";
211         } else {
212                 s += "-";
213         }
214         if (off.hours < 10) {
215                 s += "0";
216         }
217         s += off.hours;
218         if (off.minutes < 10) {
219                 s += "0";
220         }
221         s += off.minutes;
222         s += "\n";
223
224         return s;
225 }
226
227 function formatTZ(tztype, tzstart, tzrr, tzofffrom, tzoffto)
228 {
229         var icaltz = "";
230
231         icaltz += "BEGIN:" + tztype + "\n";
232         icaltz += "DTSTART:" + tzstart + "\n";
233         if (tzrr) {
234                 icaltz += tzrr.icalProperty;
235         }
236         icaltz += "TZOFFSETFROM:" + formatTZoff(tzofffrom);
237         icaltz += "TZOFFSETTO:" + formatTZoff(tzoffto);
238         icaltz += "END:" + tztype + "\n";
239
240         return icaltz;
241 }
242
243 function readMeetingTimezone(cit, it)
244 {
245         /*
246          * Timezone handling is even more iCalendar centric than recurrence
247          * handling.  We have to mock up an ical string for the timezone,
248          * then pack it into a VCALENDAR, parse the whole mess, extract back
249          * the VTIMEZONE component, and mock up a calITimezone object with the
250          * parsed icalComponent.
251          */
252         var tz = it.t::MeetingTimeZone[0];
253
254         if (!tz) {
255                 return;
256         }
257
258         var tzname = tz.@TimeZoneName.toString();
259
260         var baseoff = cal.createDuration(tz.t::BaseOffset.toString());
261         var dstoff = baseoff;
262         var stdtoff = baseoff;
263
264         var stdttimestr = "19700101T000000";
265
266         var dsttz = tz.t::Daylight[0];
267         var stdttz = tz.t::Standard[0];
268
269         if (dsttz && stdttz) {
270                 dstoff = cal.createDuration(dsttz.t::Offset);
271                 dstoff.addDuration(baseoff);
272                 var dstrr = readRecurrenceRule(dsttz.t::RelativeYearlyRecurrence);
273                 var dsttimestr = '19700101T' + dsttz.t::Time.toString().replace(/:/g, '');
274
275                 stdtoff = cal.createDuration(stdttz.t::Offset);
276                 stdtoff.addDuration(baseoff);
277                 var stdtrr = readRecurrenceRule(stdttz.t::RelativeYearlyRecurrence);
278                 stdttimestr = '19700101T' + stdttz.t::Time.toString().replace(/:/g, '');
279         }
280
281         var icaltz = "";
282
283         icaltz += "BEGIN:VCALENDAR\n";
284         icaltz += "BEGIN:VTIMEZONE\n";
285         icaltz += "TZID:" + tzname + "\n";
286         if (dsttz) {
287                 icaltz += formatTZ('DAYLIGHT', dsttimestr, dstrr, stdtoff, dstoff);
288         }
289         icaltz += formatTZ('STANDARD', stdttimestr, stdtrr, dstoff, stdtoff);
290         icaltz += "END:VTIMEZONE\n";
291         icaltz += "END:VCALENDAR\n";
292
293         var calcomp = cal.getIcsService().parseICS(icaltz, null);
294         var tzcomp = calcomp.getFirstSubcomponent("VTIMEZONE");
295         var ctz = {
296                 //provider: cprov,
297                 icalComponent: tzcomp,
298                 tzid: tzname,
299                 isFloating: false,
300                 isUTC: false,
301                 displayName: tzname,
302                 toString: function() {
303                         return tzname;
304                 }
305         };
306
307         if (dstoff.inSeconds == 0 && stdtoff.inSeconds == 0) {
308                 ctz = cal.UTC();
309         }
310
311         cit.startDate = cit.startDate.getInTimezone(ctz);
312         cit.endDate = cit.endDate.getInTimezone(ctz);
313 }
314
315 function readXmlAttendee(at, type)
316 {
317         let mbox = at.t::Mailbox;
318         let attendee = createAttendee();
319
320         if (!type) {
321                 type = "REQ-PARTICIPANT";
322         }
323
324         attendee.id = 'mailto:' + mbox.t::EmailAddress.toString();
325         attendee.commonName = mbox.t::Name.toString();
326         attendee.rsvp = "FALSE";
327         attendee.userType = "INDIVIDUAL";
328         attendee.role = type;
329
330         if (at.t::ResponseType.length() > 0) {
331                 attendee.participationStatus = participationMap[at.t::ResponseType.toString()];
332         }
333
334         return attendee;
335 }
336
337 function readDismissSnoozeState(aItem, it)
338 {
339         let nextRem = it.t::ExtendedProperty.(t::ExtendedFieldURI.@PropertyId == '34144').t::Value[0];
340
341         if (!nextRem) {
342                 return;
343         }
344
345         nextRem = cal.fromRFC3339(nextRem.toString(), cal.UTC());
346         let lastAck = nextRem.clone();
347         lastAck.addDuration(cal.createDuration('-PT1S'));
348
349         aItem.alarmLastAck = lastAck;
350
351         if (nextRem.compare(cal.fromRFC3339("4501-01-01T00:00:00Z", cal.UTC())) == 0) {
352                 return;
353         }
354
355         if (!aItem.recurrenceInfo) {
356                 aItem.setProperty('X-MOZ-SNOOZE-TIME', nextRem.icalString);
357         } else {
358                 let nextOcc = aItem.recurrenceInfo.getNextOccurrence(lastAck);
359                 let alarm = aItem.getAlarms({})[0];
360
361                 if (nextOcc && alarm) {
362                         let nextOccAlarm = cal.alarms.calculateAlarmDate(nextOcc, alarm);
363                         let alarmDiff = nextOccAlarm.compare(nextRem);
364
365                         if (alarmDiff == 0) {
366                                 /* no item was snoozed, this is the regular alarm date */
367                         } else {
368                                 if (alarmDiff > 0) {
369                                         /*
370                                          * The alarm of the next item will happen after
371                                          * the remider time.  This means we did snooze
372                                          * a previous alarm.
373                                          */
374                                         nextOcc = aItem.recurrenceInfo.getPreviousOccurrence(nextRem);
375                                 }
376                                 aItem.setProperty('X-MOZ-SNOOZE-TIME-' + nextOcc.recurrenceId.nativeTime,
377                                                   nextRem.icalString);
378                         }
379                 }
380         }
381 }
382
383 function readXmlItem(aCal, it, exitems)
384 {
385         var cit = cal.createEvent();
386         cit.calendar = aCal.superCalendar;
387
388         cit.id = it.t::UID.toString();
389         cit.setProperty("X-ITEMID", it.t::ItemId.@Id.toString());
390         cit.setProperty("SEQUENCE", it.t::ItemId.@ChangeKey.toString());
391
392         cit.title = it.t::Subject;
393         cit.setProperty("DESCRIPTION", it.t::Body.(@BodyType == "Text").toString());
394
395         var cats = [];
396         for each (var cat in it.t::Categories.t::String) {
397                 cats.push(cat.toString());
398         }
399         cit.setCategories(cats.length, cats);
400
401         if (it.t::ReminderIsSet == "true") {
402                 let alarmOffset = cal.createDuration();
403                 alarmOffset.minutes = - it.t::ReminderMinutesBeforeStart;
404                 alarmOffset.normalize();
405
406                 let alarm = cal.createAlarm();
407                 alarm.action = "DISPLAY";
408                 alarm.related = Components.interfaces.calIAlarm.ALARM_RELATED_START;
409                 alarm.offset = alarmOffset;
410
411                 cit.addAlarm(alarm);
412         }
413
414         var defaultTz = cal.calendarDefaultTimezone();
415         cit.startDate = cal.fromRFC3339(it.t::Start, defaultTz).getInTimezone(defaultTz);
416         cit.endDate = cal.fromRFC3339(it.t::End, defaultTz).getInTimezone(defaultTz);
417
418         if (it.t::IsAllDayEvent == "true") {
419                 cit.startDate.isDate = true;
420                 cit.endDate.isDate = true;
421         }
422
423         cit.setProperty("DTSTAMP", _f["fromRFC3339"](it.t::DateTimeReceived, cal.UTC()));
424         cit.setProperty("LOCATION", it.t::Location.toString());
425
426         if (it.t::Organizer.length() > 0) {
427                 let org = readXmlAttendee(it.t::Organizer);
428                 org.isOrganizer = true;
429                 cit.organizer = org;
430         }
431
432         for each (var at in it.t::RequiredAttendees.t::Attendee) {
433                 cit.addAttendee(readXmlAttendee(at), "REQ-PARTICIPANT");
434         }
435         for each (var at in it.t::OptionalAttendees.t::Attendee) {
436                 cit.addAttendee(readXmlAttendee(at), "OPT-PARTICIPANT");
437         }
438
439         var me = aCal.getInvitedAttendee(cit);
440         if (me) {
441                 var stat = it.t::MyResponseType.toString() || "Unknown";
442
443                 me.participationStatus = participationMap[stat];
444         }
445
446         cit.recurrenceId = _f["fromRFC3339"](it.t::RecurrenceId, cal.UTC());
447         cit.recurrenceInfo = readRecurrence(cit, it);
448         readModifiedOccurrences(cit, it, exitems);
449         readDeletedOccurrences(cit, it);
450
451         readMeetingTimezone(cit, it);
452
453         readDismissSnoozeState(cit, it);
454
455         return cit;
456 }
457
458 function readGetItemReply(aCal, aRep, aItemFilter, aRangeStart, aRangeEnd)
459 {
460         var rm = aRep..m::GetItemResponseMessage;
461
462         var searchtype;
463         if (aItemFilter & Components.interfaces.calICalendar.ITEM_FILTER_REQUEST_NEEDS_ACTION) {
464                 searchtype = 'MeetingRequest';
465         } else {
466                 searchtype = 'CalendarItem';
467         }
468
469         var exitems = [];
470         for each (var it in rm.m::Items.children(searchtype).(t::CalendarItemType == "Exception")) {
471                 var citem = readXmlItem(aCal, it);
472                 exitems[[it.t::ItemId.@Id, it.t::ItemId.@ChangeKey]] = citem;
473         }
474
475         var items = [];
476         for each (var it in rm.m::Items.children(searchtype).(t::CalendarItemType != "Exception")) {
477                 var citem = readXmlItem(aCal, it, exitems);
478
479                 if (aItemFilter & Components.interfaces.calICalendar.ITEM_FILTER_CLASS_OCCURRENCES) {
480                         var expanditems = citem.getOccurrencesBetween(aRangeStart, aRangeEnd, {});
481                         expanditems = expanditems || [citem];
482                         items = items.concat(expanditems);
483                 } else {
484                         items.push(citem);
485                 }
486         }
487
488         return items;
489 }
490
491 function readFindItemReply(aRep)
492 {
493         /*
494          * We want to include all Single items, all Exception items, but also
495          * at least one Occurrence or Exception item for each master.
496          * If we include too many Occurrences, we will query for the master
497          * too often, but if we don't include any, we might not query for the
498          * master at all.
499          *
500          * We first collect all non-Occurrences, and after that we fill in
501          * Occurrence for those masters that did not yet see any Exception.
502          */
503
504         var uids = {};
505         var ids = [];
506         for each (var e in aRep..t::Items.*.(t::CalendarItemType != "Occurrence")) {
507                 ids.push({Id: e.t::ItemId.@Id.toString(),
508                           ChangeKey: e.t::ItemId.@ChangeKey.toString(),
509                           type: e.t::CalendarItemType.toString(),
510                           uid: e.t::UID.toString()});
511                 uids[e.t::UID.toString()] = true;
512         }
513
514         for each (var e in aRep..t::Items.*.(t::CalendarItemType == "Occurrence")) {
515                 if (uids[e.t::UID.toString()]) {
516                         continue;
517                 }
518                 ids.push({Id: e.t::ItemId.@Id.toString(),
519                           ChangeKey: e.t::ItemId.@ChangeKey.toString(),
520                           type: e.t::CalendarItemType.toString(),
521                           uid: e.t::UID.toString()});
522                 uids[e.t::UID.toString()] = true;
523         }
524
525         return ids;
526 }