override User-Agent to force basic auth with TMG
[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 wantInvitations = 
426                         ((aItemFilter & Components.interfaces.calICalendar
427                                 .ITEM_FILTER_REQUEST_NEEDS_ACTION) != 0);
428
429                 if (!(aItemFilter & Components.interfaces
430                                 .calICalendar.ITEM_FILTER_TYPE_EVENT)
431                     && !wantInvitations) {
432                         this.completeGetRequest(aListener);
433                         return;
434                 }
435
436                 var data = makeFindItem(aItemFilter, aCount, aRangeStart, aRangeEnd);
437
438                 var req = new calExchangeRequest(this, data,
439                                                  "findItemReply");
440
441                 req.e4x = true;
442                 req.mListener = aListener;
443                 req.mOp = Components.interfaces.calIOperationListener.GET;
444                 req.mInvite = wantInvitations;
445                 req.mItemFilter = aItemFilter;
446                 req.mRangeStart = aRangeStart;
447                 req.mRangeEnd = aRangeEnd;
448
449                 this.queueRequest(req);
450         },
451
452         completeGetRequest: function(aReq)
453         {
454                 this.notifyOperationComplete(aReq.mListener,
455                       Components.results.NS_OK,
456                       Components.interfaces.calIOperationListener.GET,
457                       aReq.mListenId || null,
458                       null);
459         },
460
461         findItemReply: function(aReq, aReply)
462         {
463                 // FindItem does not return body.  Calendar's GetItem never
464                 // seems to be called, so we gotta be proactive and call
465                 // Exchange's getItem to retrieve body.
466
467                 var ids = readFindItemReply(aReply);
468
469                 this.sdbg("findItemReply: " + ids.length + "\n");
470
471                 if (ids.length == 0) {
472                         this.completeGetRequest(aReq);
473                         return;
474                 }
475
476                 var data = makeGetItem(ids, aReq.mItemFilter);
477                 var req  = new calExchangeRequest(this, data, "getItemReply");
478                 req.mListener = aReq.mListener;
479                 req.mOp = aReq.mOp;
480                 req.mInvite = aReq.mInvite;
481                 req.mItemFilter = aReq.mItemFilter;
482                 req.mRangeStart = aReq.mRangeStart;
483                 req.mRangeEnd = aReq.mRangeEnd;
484                 req.e4x = true;
485
486                 this.queueRequest(req, true);
487         },
488
489         getItemReply: function(aReq, aReply)
490         {
491                 this.sdbg("getItemReply\n");
492
493                 var el = aReq.mInvite ? "MeetingRequest" : "CalendarItem";
494
495                 var items = readGetItemReply(this, aReply, aReq.mItemFilter, aReq.mRangeStart, aReq.mRangeEnd);
496                 for each (var it in items) {
497                         it.setProperty("X-SOR-INVITE", aReq.mInvite);
498                 }
499
500                 var listener = aReq.mListener;
501
502                 listener.onGetResult(this,
503                                      Components.results.NS_OK,
504                                      Components.interfaces.calIEvent,
505                                      null,
506                                      items.length,
507                                      items);
508
509                 this.completeGetRequest(aReq);
510         },
511
512         modifyItem: function(aNewItem, aOldItem, aListener)
513         {
514                 this.sdbg("modifyItem\n");
515
516                 /* just in case we need to modify it */
517                 aNewItem = aNewItem.clone();
518
519                 var data;
520                 var del = false;
521                 var flags = {};
522
523                 var invite = aNewItem.getProperty("X-SOR-INVITE") || this.isInvitation(aNewItem);
524
525                 if (invite === true) {
526                         /* XXX only come here if the participation status changed */
527                         var me = this.getInvitedAttendee(aNewItem);
528
529                         if (me.participationStatus == "DECLINED")
530                                 del = true;
531
532                         this.sdbg("PS: " + me.participationStatus + "\n");
533
534                         data = makeMeetingResponse(aNewItem, me.participationStatus);
535
536                         // XXX
537                         var a = this.getInvitedAttendee(aOldItem)
538                                         .participationStatus;
539                         var b = me.participationStatus;
540                         if (((a == b) 
541                                 && (a == "ACCEPTED" || a == "NEEDS-ACTION"))
542                             || ((a == "TENTATIVE") && (b == "NEEDS-ACTION")))
543                                 data = "";
544                 } else
545                         data = makeUpdateItem(aNewItem, aOldItem, flags);
546
547                 if (data == "") {
548                         this.completeUpdateRequest(aListener, aNewItem,
549                                                    aOldItem);
550
551                         return;
552                 }
553
554                 var req = new calExchangeRequest(this, data,
555                                                  flags.createExceptions
556                                                      ? "updateItem_updateExceptions"
557                                                      : "updateItemReply");
558
559                 req.mListener = aListener;
560                 req.mNewItem  = aNewItem;
561                 req.mOldItem  = aOldItem;
562                 req.mOp = Components.interfaces.calIOperationListener.MODIFY;
563                 req.e4x = true;
564                 req.mDel = del;
565                 req.mFlags = flags;
566
567                 this.queueRequest(req);
568         },
569
570         /*
571          * When changing the recurrence of a master, we will lose all
572          * exception / deleted information.  This function sets out
573          * to recreate those.
574          *
575          * We will operate in two steps, each as separate request:
576          * - first we delete occurrences if necessary
577          * - then we modify all exceptions
578          */
579         updateItem_updateExceptions: function(aReq, aReply)
580         {
581                 /* build a GetItem requests to sync what Exchange did with our update */
582
583                 var newitem = aReq.mNewItem.clone();
584
585                 var newid = aReply..t::ItemId[0];
586                 if (newid) {
587                         newitem.setProperty("SEQUENCE", newid.@ChangeKey.toString());
588                 } else {
589                         /* Bad luck.  Better ask for the latest Id */
590                         var nreq = aReq;
591                         nreq.mData = makeGetItemId(newitem);
592                         this.queueRequest(nreq, true);
593                         return;
594                 }
595
596                 let nolditem = newitem.clone();
597                 //nolditem.recurrenceInfo = nolditem.recurrenceInfo.clone();
598
599                 let havedel = false;
600                 let havepos = false;
601                 let ritems = [];
602                 for each (let ritem in newitem.recurrenceInfo.getRecurrenceItems({})) {
603                         if (!cal.calInstanceOf(ritem, Components.interfaces.calIRecurrenceDate)) {
604                                 ritems.push(ritem);
605                                 continue;
606                         }
607
608                         if (ritem.isNegative) {
609                                 havedel = true;
610                         }
611                 }
612
613                 if (newitem.recurrenceInfo.getExceptionIds({}).length > 0) {
614                         havepos = true;
615                 }
616
617                 nolditem.recurrenceInfo.setRecurrenceItems(ritems.length, ritems);
618
619                 var xreq = null;
620                 var flags = aReq.mFlags;
621
622                 if (!aReq.mFlags.didDelete && havedel) {
623                         xreq = checkDeletedOccurrences(newitem, nolditem);
624                         flags.didDelete = true;
625                         flags.createExceptions = havepos;
626                 } else {
627                         let upds = new XMLList();
628                         for each (let exid in newitem.recurrenceInfo.getExceptionIds({})) {
629                                 nolditem.recurrenceInfo.removeExceptionFor(exid);
630                                 let upoitem = nolditem.recurrenceInfo.getOccurrenceFor(exid);
631                                 let upnitem = newitem.recurrenceInfo.getOccurrenceFor(exid);
632                                 let upd = makeUpdateOneItem(upnitem, upoitem);
633                                 upds += upd;
634                         }
635
636                         xreq = makeUpdateItem2(newitem, upds);
637                         flags.createExceptions = false;
638                 }
639
640                 var data = makeSoapMessage(xreq);
641
642                 var req = new calExchangeRequest(this, data,
643                                                  flags.createExceptions
644                                                      ? "updateItem_updateExceptions"
645                                                      : "updateItemReply");
646
647                 req.mListener = aReq.mListener;
648                 req.mNewItem  = aReq.mNewItem;
649                 req.mOldItem  = aReq.mOldItem;
650                 req.mOp = Components.interfaces.calIOperationListener.MODIFY;
651                 req.e4x = true;
652                 req.mDel = aReq.mDel;
653                 req.mFlags = flags;
654
655                 this.queueRequest(req, true);
656         },
657
658         updateItemReply: function(aReq, aReply)
659         {
660                 this.sdbg("updateItemReply\n");
661
662                 /* build a GetItem requests to sync what Exchange did with our update */
663
664                 var newitem = aReq.mNewItem.clone();
665
666                 var newid = aReply..t::ItemId[0];
667                 if (newid) {
668                         /*
669                          * We have to be careful.  If we (re-)set the id of the
670                          * recurring master, the calendar backend will automatically
671                          * overwrite all children's ids.
672                          */
673                         newitem.setProperty("X-ITEMID", newid.@Id.toString());
674                         newitem.setProperty("SEQUENCE", newid.@ChangeKey.toString());
675                 }
676
677                 var ids = [];
678                 if (!newitem.parentItem.recurrenceInfo) {
679                         ids.push({Id: newitem.getProperty("X-ITEMID")});
680                 } else {
681                         /*
682                          * We have to list all the exceptions of our master, or
683                          * otherwise the GetItem will reconstruct the master
684                          * incompletely.
685                          *
686                          * If we modify an exception, make sure that we
687                          * ask for it as first item, so we can notify the
688                          * listener with the right item lateron.
689                          */
690                         if (newitem.parentItem != newitem) {
691                                 ids.push({Id: newitem.getProperty("X-ITEMID"), type: "Exception"});
692                         }
693
694                         var recurrenceInfo = newitem.parentItem.recurrenceInfo;
695                         for each (let id in recurrenceInfo.getExceptionIds({})) {
696                                 if (newitem.recurrenceId && id.compare(newitem.recurrenceId) == 0) {
697                                         /* we added this one already */
698                                         continue;
699                                 }
700                                 let exitem = recurrenceInfo.getExceptionFor(id);
701                                 ids.push({Id: exitem.getProperty("X-ITEMID"), type: "Exception"});
702                         }
703
704                         /*
705                          * Seems this was an update deleting an occurrence, and there
706                          * is no other exception.  We'll have to ask for the master
707                          * then.
708                          */
709                         if (ids.length == 0) {
710                                 ids.push({Id: newitem.parentItem.getProperty("X-ITEMID")});
711                         }
712                 }
713                 var data = makeGetItem(ids);
714
715                 var req = new calExchangeRequest(this, data, "resyncUpdateItemReply");
716
717                 req.mListener = aReq.mListener;
718                 req.mNewItem  = aReq.mNewItem;
719                 req.mOldItem  = aReq.mOldItem;
720                 req.mOp = Components.interfaces.calIOperationListener.MODIFY;
721                 req.e4x = true;
722                 req.mDel = aReq.mDel;
723
724                 this.queueRequest(req, true);
725         },
726
727         resyncUpdateItemReply: function(aReq, aReply)
728         {
729                 this.sdbg("resyncUpdateItemReply\n");
730                 var items = readGetItemReply(this, aReply);
731
732                 var listener = aReq.mListener;
733                 var item = items[0];
734
735                 this.completeUpdateRequest(listener, item, aReq.mOldItem);
736
737                 if (aReq.mDel)
738                         this.mObservers.notify("onDeleteItem", [item]);
739         },
740
741         completeUpdateRequest: function(aListener, aNewItem, aOldItem)
742         {
743                 this.notifyOperationComplete(aListener,
744                                              Components.results.NS_OK,
745                                              Components.interfaces.calIOperationListener.MODIFY,
746                                              aNewItem.id,
747                                              aNewItem);
748
749                 if (aNewItem.parentItem != aNewItem) {
750                         var master = aNewItem.parentItem.clone();
751
752                         master.recurrenceInfo.modifyException(aNewItem, false);
753                         aNewItem = master;
754                         aOldItem = aOldItem.parentItem;
755                 }
756
757                 this.mObservers.notify("onModifyItem",
758                                        [aNewItem, aOldItem]);
759         },
760
761         deleteItem: function(aItem, aListener)
762         {
763                 this.sdbg("deleting item\n");
764
765                 var data = makeDeleteItem(aItem);
766
767                 var req = new calExchangeRequest(this, data,
768                                                  "deleteItemReply");
769
770                 req.mListener = aListener;
771                 req.mItem     = aItem;
772                 req.mOp = Components.interfaces.calIOperationListener.DELETE;
773
774                 this.queueRequest(req);
775         },
776
777         deleteItemReply: function(aReq, aReply)
778         {
779                 this.sdbg("deleteItemReply\n");
780
781                 var item     = aReq.mItem;
782                 var listener = aReq.mListener;
783
784                 listener.onOperationComplete(this,
785                       Components.results.NS_OK,
786                       Components.interfaces.calIOperationListener.DELETE,
787                       item.id,
788                       item);
789
790                 this.mObservers.notify("onDeleteItem", [item]);
791         },
792
793         addItem: function(aItem, aListener)
794         {
795                 return this.adoptItem(aItem.clone(), aListener);
796         },
797
798         adoptItem: function(aItem, aListener)
799         {
800                 this.sdbg("adding item\n");
801
802                 if (!aItem.id) {
803                         aItem.id = cal.getUUID();
804                 }
805
806                 var data = makeCreateItem(aItem);
807
808                 var req = new calExchangeRequest(this, data,
809                                                  "createItemReply");
810
811                 req.e4x = true;
812                 req.mListener = aListener;
813                 req.mItem     = aItem;
814                 req.mOp = Components.interfaces.calIOperationListener.ADD;
815
816                 this.queueRequest(req);
817         },
818
819         createItemReply: function(aReq, aReply)
820         {
821                 this.sdbg("createItemReply\n");
822
823                 var item     = aReq.mItem;
824                 var listener = aReq.mListener;
825
826                 var id = aReply..t::ItemId[0];
827                 item.setProperty("X-ITEMID", id.@Id.toString());
828                 item.setProperty("SEQUENCE", id.@ChangeKey.toString());
829
830                 listener.onOperationComplete(this,
831                       Components.results.NS_OK,
832                       Components.interfaces.calIOperationListener.ADD,
833                       item.id,
834                       item);
835
836                 this.mObservers.notify("onAddItem", [item]);
837         },
838
839         getFreeBusyIntervals: function(aCalId, aRangeStart, aRangeEnd,
840                                        aBusyTypes, aListener)
841         {
842                 this.sdbg("getFreeBusyIntervals: " + aCalId + "\n");
843
844                 if (aCalId.indexOf("@") < 0 || aCalId.indexOf(".") < 0) {
845                         // No valid email, screw it
846                         aListener.onResult(null, null);
847                         return;
848                 }
849
850                 var data = makeGetUserAvailability(
851                                         aCalId.replace(/^MAILTO:/, ""),
852                                         aRangeStart, aRangeEnd);
853
854                 var req = new calExchangeRequest(this, data,
855                                                  "UserAvailabilityReply");
856
857                 req.mListener = aListener;
858                 req.mAvail    = true;
859                 req.mCalId    = aCalId;
860
861                 this.queueRequest(req);
862         },
863
864         UserAvailabilityReply: function(aReq, aReply)
865         {
866                 this.sdbg("UserAvailabilityReply\n");
867
868                 var items = new Array();
869
870                 var ci = aReply.getElementsByTagNameNS(_mst, "CalendarEvent");
871                 for (i = 0; i < ci.length; i++) {
872                         var item = this.doAvailability(aReq, ci[i]);
873                         items.push(item);
874                 }
875
876                 aReq.mListener.onResult(null, items);
877         },
878
879         doAvailability: function(aReq, aCi)
880         {
881                 const x = Components.interfaces.calIFreeBusyInterval;
882
883                 const types = {
884                         "Free"          : x.FREE,
885                         "Tentative"     : x.BUSY_TENTATIVE,
886                         "Busy"          : x.BUSY,
887                         "OOF"           : x.BUSY_UNAVAILABLE,
888                         "NoData"        : x.UNKNOWN
889                 };
890
891                 /* We ask Exchange to report the time in UTC */
892
893                 var start = _f["fromRFC3339"](getXml(aCi, "StartTime", _mst), cal.UTC());
894                 var end   = _f["fromRFC3339"](getXml(aCi, "EndTime", _mst), cal.UTC());
895                 var type  = types[getXml(aCi, "BusyType", _mst)];
896
897                 return new _f["FreeBusyInterval"](aReq.mCalId, type,
898                                                   start, end);
899         },
900
901         // XXX seem to get auth prompts for NTLM.  Suppress them here.
902         getInterface: function(iid)
903         {
904                 this.sdbg("getInterface\n");
905
906                 if (iid.equals(Components.interfaces.nsIAuthPrompt)) {
907                         if (this.mAuthFail++ > 2) {
908                                 this.mAuthFail = 0;
909                                 if (!this.discardAccount()) {
910                                         return fakePrompt(this, false);
911                                 }
912                         }
913
914                         return fakePrompt(this);
915                 } 
916
917                 this.sdbg("IID: " + iid + "\n");
918
919 //              throw Components.results.NS_ERROR_NO_INTERFACE;
920                 return null;
921         },
922
923         getAccount: function()
924         {
925                 var save = true;
926
927                 this.sdbg("getAccount\n");
928
929                 var username = { value: this.mUser };
930                 var password = { value: this.mPass };
931                 var persist  = { value: false };
932                 var title = "Microsoft Exchange";
933                 var realm = this.mUri.prePath;
934
935                 var got = _f["passwordManagerGet"]
936                                 (this.mUser, password, realm, realm);
937
938                 if (got) {
939                         this.sdbg("USER: " + this.mUser + "\n");
940                         this.mPass = password.value;
941                         return true;
942                 }
943
944                 var ok = _f["getCredentials"](title, realm, username, password, persist);
945
946                 if (!ok) {
947                         this.mUser = null;
948                         this.mPass = null;
949                         return false;
950                 }
951
952                 this.mUser = username.value;
953                 this.mPass = password.value;
954
955                 this.sdbg("USER: " + this.mUser + "\n");
956
957                 //this.sdbg("PASS: " + this.mPass + "\n");
958
959                 if (persist.value) {
960                         _f["passwordManagerSave"](this.mUser, this.mPass,
961                                                   realm, realm);
962                 }
963
964                 return true;
965         },
966
967         discardAccount: function()
968         {
969                 if (!this.mUser || !this.mPass) {
970                         return false;
971                 }
972
973                 var realm = this.mUri.prePath;
974
975                 this.sdbg("discarding password info on " + realm + " / " + this.mUser + "\n");
976
977                 _f["passwordManagerRemove"](this.mUser, realm, realm);
978                 return this.getAccount();
979         },
980
981         sdbg: function(aText)
982         {
983                 var id = this.id;
984
985                 if (id === null || id === undefined) {
986                         // we will confuse the settings if we query the name too early
987                         sdbg("unknown " + this + ": " + aText);
988                         return;
989                 }
990
991                 sdbg(this.id + "/" + this.name + ": " + aText);
992         },
993
994         mReqQueue: null,
995         mServer: null,
996         mUri: null,
997         mRunning: null,
998         mUser: null,
999         mPass: null,
1000         mInit: 0,
1001         mAuthFail: 0
1002 };
1003
1004 function calExchangeRequest(aCal, aData, aCb)
1005 {
1006         this.wrappedJSObject = this;
1007         this.mCal  = aCal;
1008         this.mData = aData;
1009         this.mCb   = aCb;
1010         this.retries = 0;
1011 }
1012
1013 calExchangeRequest.prototype = {
1014         QueryInterface: function(aIID) {
1015                 return doQueryInterface(this,
1016                                         calExchangeRequest.prototype,
1017                                         aIID,
1018                                         null,
1019                                         g_classInfo["calExchangeRequest"]);
1020         },
1021
1022         mCal: null,
1023         mData: null,
1024         mCb: null,
1025         mListener: null
1026 };
1027
1028 function sendMail(aItem)
1029 {
1030         var inv = aItem.getProperty("X-MOZ-SEND-INVITATIONS");
1031
1032         if (inv == "TRUE" || true)
1033                 return "SendToAllAndSaveCopy";
1034
1035         return "SendToNone";
1036 }
1037
1038 function getXml(aXml, aName, aNs)
1039 {
1040         var items;
1041
1042         if (aNs == null)
1043                 items = aXml.getElementsByTagName(aName);
1044         else
1045                 items = aXml.getElementsByTagNameNS(aNs, aName);
1046         
1047         if (items.length == 0)
1048                 return "";
1049
1050         return items[0].textContent;
1051 }
1052
1053 function doXPath(aXml, aXPath)
1054 {
1055         var doc = aXml.ownerDocument;
1056
1057         var res = function(aPref) {
1058                 var ns = {
1059                         't' : _mst
1060                 };
1061
1062                 return ns[aPref] || null;
1063         }
1064
1065         return doc.evaluate(aXPath, aXml, res,
1066                      Components.interfaces.nsIDOMXPathResult.ANY_TYPE, null);
1067 }
1068
1069 function fakePrompt(aCal, aSuccess)
1070 {
1071         if (aSuccess === undefined) {
1072                 aSuccess = true;
1073         }
1074
1075         sdbg("making prompt: " + aSuccess + "\n");
1076
1077         var p = {
1078                 promptUsernameAndPassword: function(dialogTitle,
1079                                                     text,
1080                                                     passwordRealm,
1081                                                     savePassword,
1082                                                     user,
1083                                                     pwd)
1084                 {
1085                         user.value = aCal.mUser;
1086                         pwd.value  = aCal.mPass;
1087
1088                         return aSuccess;
1089                 }
1090         }
1091
1092         return p;
1093 }
1094
1095 function getDate(aRange)
1096 {
1097         // Requesting only a DATE returns items based on UTC. Therefore,
1098         // we make sure both start and end dates include a time and
1099         // timezone. This may not quite be what was requested, but I'd
1100         // say its a shortcoming of rfc3339.  
1101         //
1102         // [ripped comment from gdata - like many other things ;D]
1103         if (aRange) {
1104                 aRange = aRange.clone();
1105                 aRange.isDate = false;
1106         }
1107
1108         return _f["toRFC3339"](aRange);
1109 }
1110
1111 function sdbg(msg)
1112 {
1113         //return;
1114         cal.LOG(msg);
1115 }