implement dismiss and snooze
[lightning-exchange-provider: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                 cit.recurrenceInfo.modifyException(oit, true);
183                 delete exitems[key];
184         }
185 }
186
187 function readDeletedOccurrences(cit, it)
188 {
189         for each (var mit in it.t::DeletedOccurrences.children()) {
190                 cit.recurrenceInfo.removeOccurrenceAt(cal.fromRFC3339(mit.t::Start, cal.UTC()));
191         }
192 }
193
194 function formatTZoff(off)
195 {
196         var s = "";
197
198         /*
199          * For some very special reason EWA and RFC5545 use different
200          * opinions on what "UTC offset" and "offset from UTC" means.
201          */
202         if (off.isNegative) {
203                 s += "+";
204         } else {
205                 s += "-";
206         }
207         if (off.hours < 10) {
208                 s += "0";
209         }
210         s += off.hours;
211         if (off.minutes < 10) {
212                 s += "0";
213         }
214         s += off.minutes;
215         s += "\n";
216
217         return s;
218 }
219
220 function formatTZ(tztype, tzstart, tzrr, tzofffrom, tzoffto)
221 {
222         var icaltz = "";
223
224         icaltz += "BEGIN:" + tztype + "\n";
225         icaltz += "DTSTART:" + tzstart + "\n";
226         if (tzrr) {
227                 icaltz += tzrr.icalProperty;
228         }
229         icaltz += "TZOFFSETFROM:" + formatTZoff(tzofffrom);
230         icaltz += "TZOFFSETTO:" + formatTZoff(tzoffto);
231         icaltz += "END:" + tztype + "\n";
232
233         return icaltz;
234 }
235
236 function readMeetingTimezone(cit, it)
237 {
238         /*
239          * Timezone handling is even more iCalendar centric than recurrence
240          * handling.  We have to mock up an ical string for the timezone,
241          * then pack it into a VCALENDAR, parse the whole mess, extract back
242          * the VTIMEZONE component, and mock up a calITimezone object with the
243          * parsed icalComponent.
244          */
245         var tz = it.t::MeetingTimeZone[0];
246
247         if (!tz) {
248                 return;
249         }
250
251         var tzname = tz.@TimeZoneName.toString();
252
253         var baseoff = cal.createDuration(tz.t::BaseOffset.toString());
254         var dstoff = baseoff;
255         var stdtoff = baseoff;
256
257         var stdttimestr = "19700101T000000";
258
259         var dsttz = tz.t::Daylight[0];
260         var stdttz = tz.t::Standard[0];
261
262         if (dsttz && stdttz) {
263                 dstoff = cal.createDuration(dsttz.t::Offset);
264                 dstoff.addDuration(baseoff);
265                 var dstrr = readRecurrenceRule(dsttz.t::RelativeYearlyRecurrence);
266                 var dsttimestr = '19700101T' + dsttz.t::Time.toString().replace(/:/g, '');
267
268                 stdtoff = cal.createDuration(stdttz.t::Offset);
269                 stdtoff.addDuration(baseoff);
270                 var stdtrr = readRecurrenceRule(stdttz.t::RelativeYearlyRecurrence);
271                 stdttimestr = '19700101T' + stdttz.t::Time.toString().replace(/:/g, '');
272         }
273
274         var icaltz = "";
275
276         icaltz += "BEGIN:VCALENDAR\n";
277         icaltz += "BEGIN:VTIMEZONE\n";
278         icaltz += "TZID:" + tzname + "\n";
279         if (dsttz) {
280                 icaltz += formatTZ('DAYLIGHT', dsttimestr, dstrr, stdtoff, dstoff);
281         }
282         icaltz += formatTZ('STANDARD', stdttimestr, stdtrr, dstoff, stdtoff);
283         icaltz += "END:VTIMEZONE\n";
284         icaltz += "END:VCALENDAR\n";
285
286         var calcomp = cal.getIcsService().parseICS(icaltz, null);
287         var tzcomp = calcomp.getFirstSubcomponent("VTIMEZONE");
288         var ctz = {
289                 //provider: cprov,
290                 icalComponent: tzcomp,
291                 tzid: tzname,
292                 isFloating: false,
293                 isUTC: false,
294                 displayName: tzname,
295                 toString: function() {
296                         return tzname;
297                 }
298         };
299
300         if (dstoff.inSeconds == 0 && stdtoff.inSeconds == 0) {
301                 ctz = cal.UTC();
302         }
303
304         cit.startDate = cit.startDate.getInTimezone(ctz);
305         cit.endDate = cit.endDate.getInTimezone(ctz);
306 }
307
308 function readXmlAttendee(at, type)
309 {
310         let mbox = at.t::Mailbox;
311         let attendee = createAttendee();
312
313         if (!type) {
314                 type = "REQ-PARTICIPANT";
315         }
316
317         attendee.id = 'mailto:' + mbox.t::EmailAddress;
318         attendee.commonName = mbox.t::Name.toString();
319         attendee.rsvp = "FALSE";
320         attendee.userType = "INDIVIDUAL";
321         attendee.role = type;
322
323         if (at.t::ResponseType.length() > 0) {
324                 attendee.participationStatus = participationMap[at.t::ResponseType.toString()];
325         }
326
327         return attendee;
328 }
329
330 function readDismissSnoozeState(aItem, it)
331 {
332         let nextRem = it.t::ExtendedProperty.(t::ExtendedFieldURI.@PropertyId == '34144').t::Value[0];
333
334         if (!nextRem) {
335                 return;
336         }
337
338         nextRem = cal.fromRFC3339(nextRem.toString(), cal.UTC());
339         let lastAck = nextRem.clone();
340         lastAck.addDuration(cal.createDuration('-PT1S'));
341
342         aItem.alarmLastAck = lastAck;
343         if (!aItem.recurrenceInfo) {
344                 aItem.setProperty('X-MOZ-SNOOZE-TIME', nextRem.icalString);
345         } else {
346                 let nextOcc = aItem.recurrenceInfo.getNextOccurrence(lastAck);
347                 let alarm = aItem.getAlarms({})[0];
348
349                 if (nextOcc && alarm) {
350                         let nextOccAlarm = cal.alarms.calculateAlarmDate(nextOcc, alarm);
351                         let alarmDiff = nextOccAlarm.compare(nextRem);
352
353                         if (alarmDiff == 0) {
354                                 /* no item was snoozed, this is the regular alarm date */
355                         } else {
356                                 if (alarmDiff > 0) {
357                                         /*
358                                          * The alarm of the next item will happen after
359                                          * the remider time.  This means we did snooze
360                                          * a previous alarm.
361                                          */
362                                         nextOcc = aItem.recurrenceInfo.getPreviousOccurrence(nextRem);
363                                 }
364                                 aItem.setProperty('X-MOZ-SNOOZE-TIME-' + nextOcc.recurrenceId.nativeTime,
365                                                   nextRem.icalString);
366                         }
367                 }
368         }
369 }
370
371 function readXmlItem(aCal, it, exitems)
372 {
373         var cit = cal.createEvent();
374         cit.calendar = aCal.superCalendar;
375
376         cit.id = it.t::ItemId.@Id;
377         cit.setProperty("SEQUENCE", it.t::ItemId.@ChangeKey.toString());
378
379         cit.title = it.t::Subject;
380         cit.setProperty("DESCRIPTION", it.t::Body.(@BodyType == "Text").toString());
381
382         var cats = [];
383         for each (var cat in it.t::Categories.t::String) {
384                 cats.push(cat.toString());
385         }
386         cit.setCategories(cats.length, cats);
387
388         if (it.t::ReminderIsSet == "true") {
389                 let alarmOffset = cal.createDuration();
390                 alarmOffset.minutes = - it.t::ReminderMinutesBeforeStart;
391                 alarmOffset.normalize();
392
393                 let alarm = cal.createAlarm();
394                 alarm.action = "DISPLAY";
395                 alarm.related = Components.interfaces.calIAlarm.ALARM_RELATED_START;
396                 alarm.offset = alarmOffset;
397
398                 cit.addAlarm(alarm);
399         }
400
401         var defaultTz = cal.calendarDefaultTimezone();
402         cit.startDate = cal.fromRFC3339(it.t::Start, defaultTz).getInTimezone(defaultTz);
403         cit.endDate = cal.fromRFC3339(it.t::End, defaultTz).getInTimezone(defaultTz);
404
405         if (it.t::IsAllDayEvent == "true") {
406                 cit.startDate.isDate = true;
407                 cit.endDate.isDate = true;
408         }
409
410         cit.setProperty("DTSTAMP", _f["fromRFC3339"](it.t::DateTimeReceived, cal.UTC()));
411         cit.setProperty("LOCATION", it.t::Location.toString());
412
413         if (it.t::Organizer.length() > 0) {
414                 let org = readXmlAttendee(it.t::Organizer);
415                 org.isOrganizer = true;
416                 cit.organizer = org;
417         }
418
419         for each (var at in it.t::RequiredAttendees) {
420                 cit.addAttendee(readXmlAttendee(at), "REQ-PARTICIPANT");
421         }
422         for each (var at in it.t::OptionalAttendees) {
423                 cit.addAttendee(readXmlAttendee(at), "OPT-PARTICIPANT");
424         }
425
426         var me = aCal.getInvitedAttendee(cit);
427         if (me) {
428                 var stat = it.t::MyResponseType.toString() || "Unknown";
429
430                 me.participationStatus = participationMap[stat];
431         }
432
433         cit.recurrenceId = _f["fromRFC3339"](it.t::RecurrenceId, cal.UTC());
434         cit.recurrenceInfo = readRecurrence(cit, it);
435         readModifiedOccurrences(cit, it, exitems);
436         readDeletedOccurrences(cit, it);
437
438         readMeetingTimezone(cit, it);
439
440         readDismissSnoozeState(cit, it);
441
442         return cit;
443 }
444
445 function readGetItemReply(aCal, aRep, aItemFilter, aRangeStart, aRangeEnd)
446 {
447         var rm = aRep..m::GetItemResponseMessage;
448
449         var searchtype;
450         if (aItemFilter & Components.interfaces.calICalendar.ITEM_FILTER_REQUEST_NEEDS_ACTION) {
451                 searchtype = 'MeetingRequest';
452         } else {
453                 searchtype = 'CalendarItem';
454         }
455
456         var exitems = [];
457         for each (var it in rm.m::Items.children(searchtype).(t::CalendarItemType == "Exception")) {
458                 var citem = readXmlItem(aCal, it);
459                 exitems[[it.t::ItemId.@Id, it.t::ItemId.@ChangeKey]] = citem;
460         }
461
462         var items = [];
463         for each (var it in rm.m::Items.children(searchtype).(t::CalendarItemType != "Exception")) {
464                 var citem = readXmlItem(aCal, it, exitems);
465
466                 if (aItemFilter & Components.interfaces.calICalendar.ITEM_FILTER_CLASS_OCCURRENCES) {
467                         var expanditems = citem.getOccurrencesBetween(aRangeStart, aRangeEnd, {});
468                         expanditems = expanditems || [citem];
469                         items = items.concat(expanditems);
470                 } else {
471                         items.push(citem);
472                 }
473         }
474
475         return items;
476 }
477
478 function readFindItemReply(aRep)
479 {
480         /*
481          * We want to include all Single items, all Exception items, but also
482          * at least one Occurrence or Exception item for each master.
483          * If we include too many Occurrences, we will query for the master
484          * too often, but if we don't include any, we might not query for the
485          * master at all.
486          *
487          * We first collect all non-Occurrences, and after that we fill in
488          * Occurrence for those masters that did not yet see any Exception.
489          */
490
491         var uids = {};
492         var ids = [];
493         for each (var e in aRep..t::Items.*.(t::CalendarItemType != "Occurrence")) {
494                 ids.push({Id: e.t::ItemId.@Id.toString(),
495                           ChangeKey: e.t::ItemId.@ChangeKey.toString(),
496                           type: e.t::CalendarItemType.toString(),
497                           uid: e.t::UID.toString()});
498                 uids[e.t::UID.toString()] = true;
499         }
500
501         for each (var e in aRep..t::Items.*.(t::CalendarItemType == "Occurrence")) {
502                 if (uids[e.t::UID.toString()]) {
503                         continue;
504                 }
505                 ids.push({Id: e.t::ItemId.@Id.toString(),
506                           ChangeKey: e.t::ItemId.@ChangeKey.toString(),
507                           type: e.t::CalendarItemType.toString(),
508                           uid: e.t::UID.toString()});
509                 uids[e.t::UID.toString()] = true;
510         }
511
512         return ids;
513 }