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