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