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