slightly refactor error handling, prepare for custom error handlers
[lightning-exchange-provider:lightning-exchange-provider.git] / js / calExchange.js
1 /* -*- espresso-indent-level: 8; -*- */
2 /* ***** BEGIN LICENSE BLOCK *****
3  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4  * 
5  * The contents of this file are subject to the Mozilla Public License Version
6  * 1.1 (the "License"); you may not use this file except in compliance with
7  * the License. You may obtain a copy of the License at
8  * http://www.mozilla.org/MPL/
9  * 
10  * Software distributed under the License is distributed on an "AS IS" basis,
11  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12  * for the specific language governing rights and limitations under the
13  * License.
14  *
15  * The Original Code is Microsoft Exchange Calendar Provider code.
16  *
17  * The Initial Developer of the Original Code is
18  *   Andrea Bittau <a.bittau@cs.ucl.ac.uk>, University College London
19  * Portions created by the Initial Developer are Copyright (C) 2009
20  * the Initial Developer. All Rights Reserved.
21  *
22  * Alternatively, the contents of this file may be used under the terms of
23  * either the GNU General Public License Version 2 or later (the "GPL"), or
24  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
25  * in which case the provisions of the GPL or the LGPL are applicable instead
26  * of those above. If you wish to allow use of your version of this file only
27  * under the terms of either the GPL or the LGPL, and not to allow others to
28  * use your version of this file under the terms of the MPL, indicate your
29  * decision by deleting the provisions above and replace them with the notice
30  * and other provisions required by the GPL or the LGPL. If you do not delete
31  * the provisions above, a recipient may use your version of this file under
32  * the terms of any one of the MPL, the GPL or the LGPL.
33  *      
34  * ***** END LICENSE BLOCK ***** */
35
36 // URI format for setting up calendar:
37 // http://myemail@address.com@auto for auto discovery
38 //
39 // https://myemail@address.com@myexchangeserver.com/EWS/Exchange.asmx for
40 // hardcoded URL.
41 //
42 // myemail@address.com is the username to log in the Exchange server.
43
44 const _mst = "http://schemas.microsoft.com/exchange/services/2006/types";
45 const _msm = "http://schemas.microsoft.com/exchange/services/2006/messages";
46
47 function convDate(aDate)
48 {
49         var d = aDate.clone();
50
51         d.isDate = false;
52         return _f["toRFC3339"](d);
53 }
54
55 function calExchange()
56 {
57         this.initProviderBase();
58         this.mReqQueue = new Array();
59         this.mRunning = false;
60
61         this.mVersion = "unknown version";
62         let addonid = "lightning-exchange-provider@gitorious.org";
63         // from: https://developer.mozilla.org/en/Code_snippets/Miscellaneous
64         try {
65                 // Firefox 4 and later; Mozilla 2 and later
66                 Components.utils.import("resource://gre/modules/AddonManager.jsm");
67                 AddonManager.getAddonByID(addonid, function(addon) {
68                         this.mVersion = addon.version;
69                 });
70         } catch (ex) {
71                 // Firefox 3.6 and before; Mozilla 1.9.2 and before
72                 var em = Components.classes["@mozilla.org/extensions/manager;1"]
73                         .getService(Components.interfaces.nsIExtensionManager);
74                 var addon = em.getItemForID(addonid);
75                 this.mVersion = addon.version;
76         }
77 }
78
79 calExchange.prototype = {
80         __proto__: _proto,
81
82         classDescription: CLASS_NAME,
83         contractID: CONTRACT_ID,
84         classID: CLASS_ID,
85
86         implementationLanguage: Components.interfaces.nsIProgrammingLanguage.JAVASCRIPT,
87         flags: 0,
88
89         getInterfaces: function cI_cE_getInterfaces (count) {
90                 sdbg("getInterfaces called\n");
91                 var ifaces = [
92                         Components.interfaces.nsISupports,
93                         Components.interfaces.calICalendar,
94                         Components.interfaces.calISchedulingSupport,
95                         Components.interfaces.calIFreeBusyProvider,
96                         Components.interfaces.nsIClassInfo
97                 ];
98                 count.value = ifaces.length;
99                 return ifaces;
100         },
101
102         QueryInterface: function(aIID)
103         {
104                 return cal.doQueryInterface(this, calExchange.prototype, aIID, null, this);
105         },
106
107         /*
108          * Implement calIcalendar
109          */
110
111         getHelperForLanguage: function getHelperForLanguage(language)
112         {
113                 return null;
114         },
115
116         get type()
117         {
118                 return "exchange";
119         },
120
121         set uri(aUri)
122         {
123                 this.mUri = aUri;
124
125                 this.mUser = decodeURIComponent(aUri.username);
126
127                 this.sdbg("User:" + this.mUser + "\n");
128
129                 if (aUri.host != "auto") {
130                         this.mServer = decodeURIComponent(aUri.spec);
131                         this.sdbg("Server: " + this.mServer + "\n");
132                 }
133
134                 return aUri;
135         },
136
137         get uri()
138         {
139                 return this.mUri;
140         },
141
142         get providerID()
143         {
144                 return "lightning-exchange-provider@gitorious.org";
145         },
146
147
148         getProperty: function(aName)
149         {
150                 switch (aName) {
151                 case "capabilities.tasks.supported":
152                         return false;
153                 }
154
155                 return this.__proto__.__proto__.getProperty.apply(this,
156                                                                   arguments);
157         },
158
159         get canRefresh()
160         {
161                 return true;
162         },
163
164         refresh: function()
165         {
166                 this.mObservers.notify("onLoad", [this]);
167         },
168
169         canNotify: function(aMethod, aItem)
170         {
171                 return true;
172         },
173
174         init: function()
175         {
176                 if (this.mInit != 0)
177                         return;
178
179                 this.mInit = 1;
180                 this.sdbg("Init\n");
181
182                 getFreeBusyService().addProvider(this);
183
184                 var tmp = this;
185
186                 let timerCallback = {
187                         notify: function setTimeout_notify() {
188                                 tmp.doInit();
189                         }
190                 };
191
192                 let timer = Components.classes["@mozilla.org/timer;1"]
193                                 .createInstance(Components.interfaces.nsITimer);
194
195                 timer.initWithCallback(timerCallback, 0, timer.TYPE_ONE_SHOT);
196         },
197
198         doInit: function()
199         {
200                 this.sdbg("do init\n");
201                 this.getAccount();
202
203                 if (this.mServer == null) {
204                         this.autodiscover();
205                         return;
206                 }
207
208                 this.mInit = 2;
209                 this.kick();
210                 this.sdbg("do init done\n");
211         },
212
213         autodiscover: function()
214         {
215                 // XXX should do:
216                 // 1) domain/autodiscover/autodiscover.xml
217                 // 2) autodiscover.domain/autodiscover/autodiscover.xml
218
219                 var email;
220                 var identity = this.getProperty('imip.identity');
221                 if (identity) {
222                         email = identity.QueryInterface(Components.interfaces.nsIMsgIdentity).email;
223                 }
224                 email = email || decodeURIComponent(mUri.username);
225
226                 this.sdbg("autodiscover\n");
227
228                 var parts = email.split("@");
229                 var url = "https://autodiscover."
230                           + parts[1] 
231                           + "/autodiscover/autodiscover.xml";
232
233                 var data = makeAutodiscover(email);
234                 var req  = new calExchangeRequest(this, data,
235                                                   "autodiscoverReply");
236                 req.e4x = true;
237
238                 this.sendRequest(req, url);
239         },
240
241         autodiscoverReply: function(aReq, aReply)
242         {
243                 this.sdbg("autodiscoverreply");
244                 this.mServer = readAutodiscoverReply(aReply);
245
246                 if (!this.mServer) {
247                         this.sdbg("no server found\n");
248                 }
249
250                 ASSERT(this.mServer != "");
251
252                 this.sdbg("Autodiscover server: " + this.mServer + "\n");
253
254                 this.mInit = 2;
255                 this.kick();
256         },
257
258         sendRequest: function(aReq, aUrl)
259         {
260                 if (!aUrl) {
261                         aUrl = this.mServer;
262                 }
263
264                 this.sdbg("setup req to " + aUrl + " user " + this.mUser + "\n");
265
266                 var xmlReq =
267                   Components.classes ["@mozilla.org/xmlextras/xmlhttprequest;1"]
268                    .createInstance(Components.interfaces.nsIJSXMLHttpRequest);
269
270                 aReq.mXmlReq = xmlReq;
271
272                 var tmp = this;
273
274                 xmlReq.onreadystatechange = function() {
275                         tmp.reply(aReq);
276                 }
277
278                 xmlReq.open("POST", aUrl, true, this.mUser, this.mPass);
279
280                 xmlReq.overrideMimeType('text/xml');
281                 xmlReq.setRequestHeader("Content-Type", "text/xml");
282                 xmlReq.setRequestHeader("User-Agent", "Thunderbird Lightning Exchange Provider/" + this.mVersion);
283
284                 /* set channel notifications for password processing */
285                 xmlReq.channel.notificationCallbacks = this;
286                 xmlReq.channel.loadGroup = null;
287
288                 var httpChannel = xmlReq.channel.QueryInterface(Components.interfaces.nsIHttpChannel);
289
290                 // XXX we want to preserve POST across 302 redirects
291                 httpChannel.redirectionLimit = 0;
292
293                 this.sdbg("Sending: " + aReq.mData + "\n");
294                 xmlReq.send(aReq.mData);
295         },
296
297         queueRequest: function(aReq, aFront)
298         {
299                 this.sdbg("queueRequest\n");
300                 if (aFront) {
301                         this.mReqQueue.unshift(aReq);
302                 } else {
303                         this.mReqQueue.push(aReq);
304                 }
305                 this.kick();
306         },
307
308         kick: function()
309         {
310                 if (this.mInit != 2)
311                         return;
312
313                 if (this.mRunning
314                     || this.mReqQueue.length == 0)
315                         return;
316
317                 this.mRunning = true;
318
319                 let reqs = this.mReqQueue;
320                 this.mReqQueue = [];
321
322                 //var req = this.mReqQueue.shift();
323
324                 for each (let req in reqs) {
325                         this.sendRequest(req);
326                 }
327
328                 this.mRunning = false;
329         },
330
331         reply: function(aReq)
332         {
333                 let xmlReq = aReq.mXmlReq;
334
335                 if (xmlReq.readyState != 4)
336                         return;
337
338                 if (xmlReq.status == 302) {
339                         let httpChannel = xmlReq.channel.QueryInterface(Components.interfaces.nsIHttpChannel);
340                         let loc = httpChannel.getResponseHeader("Location");
341
342                         this.sdbg("Redirect: " + loc + "\n");
343
344                         // XXX pheer loops.
345                         xmlReq.abort();
346                         this.sendRequest(aReq, loc);
347
348                         return;
349                 }
350
351                 this.sdbg("reply\n");
352
353                 if (this.isConnError(aReq)) {
354                         this.kick();
355                         return;
356                 }
357
358                 var xml = xmlReq.responseText; // bug 270553
359                 xml = xml.replace(/^<\?xml\s+version\s*=\s*(?:"[^"]+"|'[^']+')[^?]*\?>/, ""); // bug 336551
360                 xml = new XML(xml);
361
362                 this.sdbg(String(xml) + "\n");
363
364                 this.mAuthFail = 0;
365                 this.mRunning  = false;
366
367                 var resp;
368                 if (aReq.e4x) {
369                         resp = xml;
370                 } else {
371                         resp = xmlReq.responseXML;
372                 }
373
374                 if (!this.isError(aReq)) {
375                         this[aReq.mCb](aReq, resp);
376                 }
377
378                 this.kick();
379         },
380
381         isConnError: function(aReq)
382         {
383                 let xmlReq = aReq.mXmlReq;
384
385                 if (xmlReq.status == 0) {
386                         this.sdbg("connection failure\n");
387                         xmlReq.abort();
388
389                         if (aReq.errorHandler) {
390                                 this[aReq.errorHandler](aReq);
391                                 this.kick();
392                                 return true;
393                         }
394
395                         if (aReq.retries < 3) {
396                                 aReq.retries += 1;
397                                 this.sendRequest(aReq);
398                                 return true;
399                         }
400
401                         this.fail(aReq, Components.interfaces.calIErrors.MODIFICATION_FAILED, "connection failure");
402                         xmlReq.abort();
403                         this.kick();
404                         return true;
405                 }
406
407                 if (xmlReq.status != 200) {
408                         if (xmlReq.status == 401) {
409                                 this.sdbg(xmlReq.responseText + "\n");
410                                 this.discardAccount();
411                         }
412
413                         // XXX parse it
414                         this.fail(aReq, Components.interfaces.calIErrors.MODIFICATION_FAILED, xmlReq.responseText);
415                         return true;
416                 }
417
418                 return false;
419         },
420
421         isError: function(aReq)
422         {
423                 let xmlReq = aReq.mXmlReq;
424
425                 var aReply = xmlReq.responseXML;
426
427                 var err = getXml(aReply, "ResponseCode", _msm);
428
429                 if (err == "" || err == "NoError")
430                         return false;
431
432                 var msg = getXml(aReply, "MessageText", _msm); 
433
434                 this.sdbg("Error: " + err + " msg: " + msg + "\n");
435
436                 this.fail(aReq, Components.interfaces.calIErrors.MODIFICATION_FAILED, msg);
437                 return true;
438         },
439
440         fail: function(aReq, aCode, aMsg)
441         {
442                 this.sdbg("fail status: " + aReq.mXmlReq.status + " " + aMsg + "\n");
443
444                 if (!aReq.mListener) {
445                         return;
446                 }
447
448                 if (aReq.mAvail != null) {
449                         aReq.mListener.onResult(null, null);
450
451                         var xml = aReq.mXmlReq.responseXML;
452                         var err = getXml(xml, "ResponseCode", _msm);
453
454                         // XXX should avoid the second one
455                         if (err == "ErrorMailRecipientNotFound"
456                             || err == "ErrorInvalidSmtpAddress")
457                                 return;
458                 } else {
459                         aReq.mListener.onOperationComplete(this, aCode, aReq.mOp, null, aMsg);
460                 }
461         },
462
463         getItem: function (aId, aListener)
464         {
465                 var data = makeFindItemUID(aId);
466
467                 var req = new calExchangeRequest(this, data,
468                                                  "findItemReply");
469
470                 req.e4x = true;
471                 req.mListener = aListener;
472                 req.mListenId = aId;
473                 req.mOp = Components.interfaces.calIOperationListener.GET;
474
475                 this.queueRequest(req);
476         },
477
478         getItems: function(aItemFilter, aCount, aRangeStart, aRangeEnd,
479                            aListener)
480         {
481                 this.init();
482
483                 var wantEvents =
484                         ((aItemFilter & Components.interfaces.calICalendar
485                                 .ITEM_FILTER_TYPE_EVENT) != 0);
486
487                 var wantInvitations = 
488                         ((aItemFilter & Components.interfaces.calICalendar
489                                 .ITEM_FILTER_REQUEST_NEEDS_ACTION) != 0);
490
491                 if (!wantEvents && !wantInvitations) {
492                         this.notifyOperationComplete(aListener,
493                                 Components.results.NS_OK,
494                                 Components.interfaces.calIOperationListener.GET,
495                                 null, null);
496                         return;
497                 }
498
499                 var data = makeFindItem(aItemFilter, aCount, aRangeStart, aRangeEnd);
500
501                 var req = new calExchangeRequest(this, data,
502                                                  "findItemReply");
503
504                 req.e4x = true;
505                 req.mListener = aListener;
506                 req.mOp = Components.interfaces.calIOperationListener.GET;
507                 req.mInvite = wantInvitations;
508                 req.mItemFilter = aItemFilter;
509                 req.mRangeStart = aRangeStart;
510                 req.mRangeEnd = aRangeEnd;
511
512                 this.queueRequest(req);
513         },
514
515         completeGetRequest: function(aReq)
516         {
517                 this.notifyOperationComplete(aReq.mListener,
518                       Components.results.NS_OK,
519                       Components.interfaces.calIOperationListener.GET,
520                       aReq.mListenId || null,
521                       null);
522         },
523
524         findItemReply: function(aReq, aReply)
525         {
526                 // FindItem does not return body.  Calendar's GetItem never
527                 // seems to be called, so we gotta be proactive and call
528                 // Exchange's getItem to retrieve body.
529
530                 var ids = readFindItemReply(aReply);
531
532                 this.sdbg("findItemReply: " + ids.length + "\n");
533
534                 if (ids.length == 0) {
535                         this.completeGetRequest(aReq);
536                         return;
537                 }
538
539                 var data = makeGetItem(ids, aReq.mItemFilter);
540                 var req  = new calExchangeRequest(this, data, "getItemReply");
541                 req.mListener = aReq.mListener;
542                 req.mOp = aReq.mOp;
543                 req.mInvite = aReq.mInvite;
544                 req.mItemFilter = aReq.mItemFilter;
545                 req.mRangeStart = aReq.mRangeStart;
546                 req.mRangeEnd = aReq.mRangeEnd;
547                 req.e4x = true;
548
549                 this.queueRequest(req, true);
550         },
551
552         getItemReply: function(aReq, aReply)
553         {
554                 this.sdbg("getItemReply\n");
555
556                 var el = aReq.mInvite ? "MeetingRequest" : "CalendarItem";
557
558                 var items = readGetItemReply(this, aReply, aReq.mItemFilter, aReq.mRangeStart, aReq.mRangeEnd);
559                 for each (var it in items) {
560                         it.setProperty("X-SOR-INVITE", aReq.mInvite);
561                 }
562
563                 var listener = aReq.mListener;
564
565                 listener.onGetResult(this,
566                                      Components.results.NS_OK,
567                                      Components.interfaces.calIEvent,
568                                      null,
569                                      items.length,
570                                      items);
571
572                 this.completeGetRequest(aReq);
573         },
574
575         modifyItem: function(aNewItem, aOldItem, aListener)
576         {
577                 this.sdbg("modifyItem\n");
578
579                 /* just in case we need to modify it */
580                 aNewItem = aNewItem.clone();
581
582                 var data;
583                 var del = false;
584                 var flags = {};
585
586                 var invite = aNewItem.getProperty("X-SOR-INVITE") || this.isInvitation(aNewItem);
587
588                 if (invite === true) {
589                         /* XXX only come here if the participation status changed */
590                         var me = this.getInvitedAttendee(aNewItem);
591
592                         if (me.participationStatus == "DECLINED")
593                                 del = true;
594
595                         this.sdbg("PS: " + me.participationStatus + "\n");
596
597                         data = makeMeetingResponse(aNewItem, me.participationStatus);
598
599                         // XXX
600                         var a = this.getInvitedAttendee(aOldItem)
601                                         .participationStatus;
602                         var b = me.participationStatus;
603                         if (((a == b) 
604                                 && (a == "ACCEPTED" || a == "NEEDS-ACTION"))
605                             || ((a == "TENTATIVE") && (b == "NEEDS-ACTION")))
606                                 data = "";
607                 } else
608                         data = makeUpdateItem(aNewItem, aOldItem, flags);
609
610                 if (data == "") {
611                         this.completeUpdateRequest(aListener, aNewItem,
612                                                    aOldItem);
613
614                         return;
615                 }
616
617                 var req = new calExchangeRequest(this, data,
618                                                  flags.createExceptions
619                                                      ? "updateItem_updateExceptions"
620                                                      : "updateItemReply");
621
622                 req.mListener = aListener;
623                 req.mNewItem  = aNewItem;
624                 req.mOldItem  = aOldItem;
625                 req.mOp = Components.interfaces.calIOperationListener.MODIFY;
626                 req.e4x = true;
627                 req.mDel = del;
628                 req.mFlags = flags;
629
630                 this.queueRequest(req);
631         },
632
633         /*
634          * When changing the recurrence of a master, we will lose all
635          * exception / deleted information.  This function sets out
636          * to recreate those.
637          *
638          * We will operate in two steps, each as separate request:
639          * - first we delete occurrences if necessary
640          * - then we modify all exceptions
641          */
642         updateItem_updateExceptions: function(aReq, aReply)
643         {
644                 /* build a GetItem requests to sync what Exchange did with our update */
645
646                 var newitem = aReq.mNewItem.clone();
647
648                 var newid = aReply..t::ItemId[0];
649                 if (newid) {
650                         newitem.setProperty("SEQUENCE", newid.@ChangeKey.toString());
651                 } else {
652                         /* Bad luck.  Better ask for the latest Id */
653                         var nreq = aReq;
654                         nreq.mData = makeGetItemId(newitem);
655                         this.queueRequest(nreq, true);
656                         return;
657                 }
658
659                 let nolditem = newitem.clone();
660                 //nolditem.recurrenceInfo = nolditem.recurrenceInfo.clone();
661
662                 let havedel = false;
663                 let havepos = false;
664                 let ritems = [];
665                 for each (let ritem in newitem.recurrenceInfo.getRecurrenceItems({})) {
666                         if (!cal.calInstanceOf(ritem, Components.interfaces.calIRecurrenceDate)) {
667                                 ritems.push(ritem);
668                                 continue;
669                         }
670
671                         if (ritem.isNegative) {
672                                 havedel = true;
673                         }
674                 }
675
676                 if (newitem.recurrenceInfo.getExceptionIds({}).length > 0) {
677                         havepos = true;
678                 }
679
680                 nolditem.recurrenceInfo.setRecurrenceItems(ritems.length, ritems);
681
682                 var xreq = null;
683                 var flags = aReq.mFlags;
684
685                 if (!aReq.mFlags.didDelete && havedel) {
686                         xreq = checkDeletedOccurrences(newitem, nolditem);
687                         flags.didDelete = true;
688                         flags.createExceptions = havepos;
689                 } else {
690                         let upds = new XMLList();
691                         for each (let exid in newitem.recurrenceInfo.getExceptionIds({})) {
692                                 nolditem.recurrenceInfo.removeExceptionFor(exid);
693                                 let upoitem = nolditem.recurrenceInfo.getOccurrenceFor(exid);
694                                 let upnitem = newitem.recurrenceInfo.getOccurrenceFor(exid);
695                                 let upd = makeUpdateOneItem(upnitem, upoitem);
696                                 upds += upd;
697                         }
698
699                         xreq = makeUpdateItem2(newitem, upds);
700                         flags.createExceptions = false;
701                 }
702
703                 var data = makeSoapMessage(xreq);
704
705                 var req = new calExchangeRequest(this, data,
706                                                  flags.createExceptions
707                                                      ? "updateItem_updateExceptions"
708                                                      : "updateItemReply");
709
710                 req.mListener = aReq.mListener;
711                 req.mNewItem  = aReq.mNewItem;
712                 req.mOldItem  = aReq.mOldItem;
713                 req.mOp = Components.interfaces.calIOperationListener.MODIFY;
714                 req.e4x = true;
715                 req.mDel = aReq.mDel;
716                 req.mFlags = flags;
717
718                 this.queueRequest(req, true);
719         },
720
721         updateItemReply: function(aReq, aReply)
722         {
723                 this.sdbg("updateItemReply\n");
724
725                 /* build a GetItem requests to sync what Exchange did with our update */
726
727                 var newitem = aReq.mNewItem.clone();
728
729                 var newid = aReply..t::ItemId[0];
730                 if (newid) {
731                         /*
732                          * We have to be careful.  If we (re-)set the id of the
733                          * recurring master, the calendar backend will automatically
734                          * overwrite all children's ids.
735                          */
736                         newitem.setProperty("X-ITEMID", newid.@Id.toString());
737                         newitem.setProperty("SEQUENCE", newid.@ChangeKey.toString());
738                 }
739
740                 var ids = [];
741                 if (!newitem.parentItem.recurrenceInfo) {
742                         ids.push({Id: newitem.getProperty("X-ITEMID")});
743                 } else {
744                         /*
745                          * We have to list all the exceptions of our master, or
746                          * otherwise the GetItem will reconstruct the master
747                          * incompletely.
748                          *
749                          * If we modify an exception, make sure that we
750                          * ask for it as first item, so we can notify the
751                          * listener with the right item lateron.
752                          */
753                         if (newitem.parentItem != newitem) {
754                                 ids.push({Id: newitem.getProperty("X-ITEMID"), type: "Exception"});
755                         }
756
757                         var recurrenceInfo = newitem.parentItem.recurrenceInfo;
758                         for each (let id in recurrenceInfo.getExceptionIds({})) {
759                                 if (newitem.recurrenceId && id.compare(newitem.recurrenceId) == 0) {
760                                         /* we added this one already */
761                                         continue;
762                                 }
763                                 let exitem = recurrenceInfo.getExceptionFor(id);
764                                 ids.push({Id: exitem.getProperty("X-ITEMID"), type: "Exception"});
765                         }
766
767                         /*
768                          * Seems this was an update deleting an occurrence, and there
769                          * is no other exception.  We'll have to ask for the master
770                          * then.
771                          */
772                         if (ids.length == 0) {
773                                 ids.push({Id: newitem.parentItem.getProperty("X-ITEMID")});
774                         }
775                 }
776                 var data = makeGetItem(ids);
777
778                 var req = new calExchangeRequest(this, data, "resyncUpdateItemReply");
779
780                 req.mListener = aReq.mListener;
781                 req.mNewItem  = aReq.mNewItem;
782                 req.mOldItem  = aReq.mOldItem;
783                 req.mOp = Components.interfaces.calIOperationListener.MODIFY;
784                 req.e4x = true;
785                 req.mDel = aReq.mDel;
786
787                 this.queueRequest(req, true);
788         },
789
790         resyncUpdateItemReply: function(aReq, aReply)
791         {
792                 this.sdbg("resyncUpdateItemReply\n");
793                 var items = readGetItemReply(this, aReply);
794
795                 var listener = aReq.mListener;
796                 var item = items[0];
797
798                 this.completeUpdateRequest(listener, item, aReq.mOldItem);
799
800                 if (aReq.mDel)
801                         this.mObservers.notify("onDeleteItem", [item]);
802         },
803
804         completeUpdateRequest: function(aListener, aNewItem, aOldItem)
805         {
806                 this.notifyOperationComplete(aListener,
807                                              Components.results.NS_OK,
808                                              Components.interfaces.calIOperationListener.MODIFY,
809                                              aNewItem.id,
810                                              aNewItem);
811
812                 if (aNewItem.parentItem != aNewItem) {
813                         var master = aNewItem.parentItem.clone();
814
815                         master.recurrenceInfo.modifyException(aNewItem, false);
816                         aNewItem = master;
817                         aOldItem = aOldItem.parentItem;
818                 }
819
820                 this.mObservers.notify("onModifyItem",
821                                        [aNewItem, aOldItem]);
822         },
823
824         deleteItem: function(aItem, aListener)
825         {
826                 this.sdbg("deleting item\n");
827
828                 var data = makeDeleteItem(aItem);
829
830                 var req = new calExchangeRequest(this, data,
831                                                  "deleteItemReply");
832
833                 req.mListener = aListener;
834                 req.mItem     = aItem;
835                 req.mOp = Components.interfaces.calIOperationListener.DELETE;
836
837                 this.queueRequest(req);
838         },
839
840         deleteItemReply: function(aReq, aReply)
841         {
842                 this.sdbg("deleteItemReply\n");
843
844                 var item     = aReq.mItem;
845                 var listener = aReq.mListener;
846
847                 listener.onOperationComplete(this,
848                       Components.results.NS_OK,
849                       Components.interfaces.calIOperationListener.DELETE,
850                       item.id,
851                       item);
852
853                 this.mObservers.notify("onDeleteItem", [item]);
854         },
855
856         addItem: function(aItem, aListener)
857         {
858                 return this.adoptItem(aItem.clone(), aListener);
859         },
860
861         adoptItem: function(aItem, aListener)
862         {
863                 this.sdbg("adding item\n");
864
865                 if (!aItem.id) {
866                         aItem.id = cal.getUUID();
867                 }
868
869                 var data = makeCreateItem(aItem);
870
871                 var req = new calExchangeRequest(this, data,
872                                                  "createItemReply");
873
874                 req.e4x = true;
875                 req.mListener = aListener;
876                 req.mItem     = aItem;
877                 req.mOp = Components.interfaces.calIOperationListener.ADD;
878
879                 this.queueRequest(req);
880         },
881
882         createItemReply: function(aReq, aReply)
883         {
884                 this.sdbg("createItemReply\n");
885
886                 var item     = aReq.mItem;
887                 var listener = aReq.mListener;
888
889                 var id = aReply..t::ItemId[0];
890                 item.setProperty("X-ITEMID", id.@Id.toString());
891                 item.setProperty("SEQUENCE", id.@ChangeKey.toString());
892
893                 listener.onOperationComplete(this,
894                       Components.results.NS_OK,
895                       Components.interfaces.calIOperationListener.ADD,
896                       item.id,
897                       item);
898
899                 this.mObservers.notify("onAddItem", [item]);
900         },
901
902         getFreeBusyIntervals: function(aCalId, aRangeStart, aRangeEnd,
903                                        aBusyTypes, aListener)
904         {
905                 this.sdbg("getFreeBusyIntervals: " + aCalId + "\n");
906
907                 if (aCalId.indexOf("@") < 0 || aCalId.indexOf(".") < 0) {
908                         // No valid email, screw it
909                         aListener.onResult(null, null);
910                         return;
911                 }
912
913                 var data = makeGetUserAvailability(
914                                         aCalId.replace(/^MAILTO:/, ""),
915                                         aRangeStart, aRangeEnd);
916
917                 var req = new calExchangeRequest(this, data,
918                                                  "UserAvailabilityReply");
919
920                 req.mListener = aListener;
921                 req.mAvail    = true;
922                 req.mCalId    = aCalId;
923
924                 this.queueRequest(req);
925         },
926
927         UserAvailabilityReply: function(aReq, aReply)
928         {
929                 this.sdbg("UserAvailabilityReply\n");
930
931                 var items = new Array();
932
933                 var ci = aReply.getElementsByTagNameNS(_mst, "CalendarEvent");
934                 for (i = 0; i < ci.length; i++) {
935                         var item = this.doAvailability(aReq, ci[i]);
936                         items.push(item);
937                 }
938
939                 aReq.mListener.onResult(null, items);
940         },
941
942         doAvailability: function(aReq, aCi)
943         {
944                 const x = Components.interfaces.calIFreeBusyInterval;
945
946                 const types = {
947                         "Free"          : x.FREE,
948                         "Tentative"     : x.BUSY_TENTATIVE,
949                         "Busy"          : x.BUSY,
950                         "OOF"           : x.BUSY_UNAVAILABLE,
951                         "NoData"        : x.UNKNOWN
952                 };
953
954                 /* We ask Exchange to report the time in UTC */
955
956                 var start = _f["fromRFC3339"](getXml(aCi, "StartTime", _mst), cal.UTC());
957                 var end   = _f["fromRFC3339"](getXml(aCi, "EndTime", _mst), cal.UTC());
958                 var type  = types[getXml(aCi, "BusyType", _mst)];
959
960                 return new _f["FreeBusyInterval"](aReq.mCalId, type,
961                                                   start, end);
962         },
963
964         // XXX seem to get auth prompts for NTLM.  Suppress them here.
965         getInterface: function(iid)
966         {
967                 this.sdbg("getInterface\n");
968
969                 if (iid.equals(Components.interfaces.nsIAuthPrompt)) {
970                         if (this.mAuthFail++ > 2) {
971                                 this.mAuthFail = 0;
972                                 if (!this.discardAccount()) {
973                                         return fakePrompt(this, false);
974                                 }
975                         }
976
977                         return fakePrompt(this);
978                 } 
979
980                 this.sdbg("IID: " + iid + "\n");
981
982 //              throw Components.results.NS_ERROR_NO_INTERFACE;
983                 return null;
984         },
985
986         getAccount: function()
987         {
988                 var save = true;
989
990                 this.sdbg("getAccount\n");
991
992                 var username = { value: this.mUser };
993                 var password = { value: this.mPass };
994                 var persist  = { value: false };
995                 var title = "Microsoft Exchange";
996                 var realm = this.mUri.prePath;
997
998                 var got = _f["passwordManagerGet"]
999                                 (this.mUser, password, realm, realm);
1000
1001                 if (got) {
1002                         this.sdbg("USER: " + this.mUser + "\n");
1003                         this.mPass = password.value;
1004                         return true;
1005                 }
1006
1007                 var ok = _f["getCredentials"](title, realm, username, password, persist);
1008
1009                 if (!ok) {
1010                         this.mUser = null;
1011                         this.mPass = null;
1012                         return false;
1013                 }
1014
1015                 this.mUser = username.value;
1016                 this.mPass = password.value;
1017
1018                 this.sdbg("USER: " + this.mUser + "\n");
1019
1020                 //this.sdbg("PASS: " + this.mPass + "\n");
1021
1022                 if (persist.value) {
1023                         _f["passwordManagerSave"](this.mUser, this.mPass,
1024                                                   realm, realm);
1025                 }
1026
1027                 return true;
1028         },
1029
1030         discardAccount: function()
1031         {
1032                 if (!this.mUser || !this.mPass) {
1033                         return false;
1034                 }
1035
1036                 var realm = this.mUri.prePath;
1037
1038                 this.sdbg("discarding password info on " + realm + " / " + this.mUser + "\n");
1039
1040                 _f["passwordManagerRemove"](this.mUser, realm, realm);
1041                 return this.getAccount();
1042         },
1043
1044         sdbg: function(aText)
1045         {
1046                 var id = this.id;
1047
1048                 if (id === null || id === undefined) {
1049                         // we will confuse the settings if we query the name too early
1050                         sdbg("unknown " + this + ": " + aText);
1051                         return;
1052                 }
1053
1054                 sdbg(this.id + "/" + this.name + ": " + aText);
1055         },
1056
1057         mReqQueue: null,
1058         mServer: null,
1059         mUri: null,
1060         mRunning: null,
1061         mUser: null,
1062         mPass: null,
1063         mInit: 0,
1064         mAuthFail: 0
1065 };
1066
1067 function calExchangeRequest(aCal, aData, aCb)
1068 {
1069         this.wrappedJSObject = this;
1070         this.mCal  = aCal;
1071         this.mData = aData;
1072         this.mCb   = aCb;
1073         this.retries = 0;
1074 }
1075
1076 calExchangeRequest.prototype = {
1077         QueryInterface: function(aIID) {
1078                 return cal.doQueryInterface(this, calExchangeRequest.prototype, aIID, null, this);
1079         },
1080
1081         mCal: null,
1082         mData: null,
1083         mCb: null,
1084         mListener: null
1085 };
1086
1087 function sendMail(aItem)
1088 {
1089         var inv = aItem.getProperty("X-MOZ-SEND-INVITATIONS");
1090
1091         if (inv == "TRUE" || inv == true)
1092                 return "SendToAllAndSaveCopy";
1093
1094         return "SendToNone";
1095 }
1096
1097 function getXml(aXml, aName, aNs)
1098 {
1099         var items;
1100
1101         if (aNs == null)
1102                 items = aXml.getElementsByTagName(aName);
1103         else
1104                 items = aXml.getElementsByTagNameNS(aNs, aName);
1105         
1106         if (items.length == 0)
1107                 return "";
1108
1109         return items[0].textContent;
1110 }
1111
1112 function doXPath(aXml, aXPath)
1113 {
1114         var doc = aXml.ownerDocument;
1115
1116         var res = function(aPref) {
1117                 var ns = {
1118                         't' : _mst
1119                 };
1120
1121                 return ns[aPref] || null;
1122         }
1123
1124         return doc.evaluate(aXPath, aXml, res,
1125                      Components.interfaces.nsIDOMXPathResult.ANY_TYPE, null);
1126 }
1127
1128 function fakePrompt(aCal, aSuccess)
1129 {
1130         if (aSuccess === undefined) {
1131                 aSuccess = true;
1132         }
1133
1134         sdbg("making prompt: " + aSuccess + "\n");
1135
1136         var p = {
1137                 promptUsernameAndPassword: function(dialogTitle,
1138                                                     text,
1139                                                     passwordRealm,
1140                                                     savePassword,
1141                                                     user,
1142                                                     pwd)
1143                 {
1144                         user.value = aCal.mUser;
1145                         pwd.value  = aCal.mPass;
1146
1147                         return aSuccess;
1148                 }
1149         }
1150
1151         return p;
1152 }
1153
1154 function getDate(aRange)
1155 {
1156         // Requesting only a DATE returns items based on UTC. Therefore,
1157         // we make sure both start and end dates include a time and
1158         // timezone. This may not quite be what was requested, but I'd
1159         // say its a shortcoming of rfc3339.  
1160         //
1161         // [ripped comment from gdata - like many other things ;D]
1162         if (aRange) {
1163                 aRange = aRange.clone();
1164                 aRange.isDate = false;
1165         }
1166
1167         return _f["toRFC3339"](aRange);
1168 }
1169
1170 function sdbg(msg)
1171 {
1172         //return;
1173         cal.LOG(msg);
1174 }