This file is indexed.

/usr/lib/thunderbird-addons/extensions/{a62ef8ec-5fdc-40c2-873c-223b8a6925cc}/js/calGoogleUtils.js is in xul-ext-gdata-provider 1:24.4.0+build1-0ubuntu1.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

Components.utils.import("resource://calendar/modules/calUtils.jsm");
Components.utils.import("resource://calendar/modules/calXMLUtils.jsm");
Components.utils.import("resource://calendar/modules/calIteratorUtils.jsm");
Components.utils.import("resource://calendar/modules/calProviderUtils.jsm");

const atomNS = "http://www.w3.org/2005/Atom";
const gdNS = "http://schemas.google.com/g/2005";
const gcalNS = "http://schemas.google.com/gCal/2005";

function gdataNSResolver(prefix) {
    const ns = {
        atom: atomNS,
        gd: gdNS,
        gCal: gcalNS
    };

    return ns[prefix] || atomNS;
}

function gdataXPath(aNode, aExpr, aType) {
    return cal.xml.evalXPath(aNode, aExpr, gdataNSResolver, aType);
}
function gdataXPathFirst(aNode, aExpr, aType) {
    // Different than the caldav/ics functions, this one will return an empty string on null
    return cal.xml.evalXPathFirst(aNode, aExpr, gdataNSResolver, aType) || "";
}

/**
 * getGoogleSessionManager
 * Shortcut to the google session manager
 */
function getGoogleSessionManager() {
    if (this.mObject === undefined) {
        this.mObject =
            Components.classes["@mozilla.org/calendar/providers/gdata/session-manager;1"]
                      .createInstance(Components.interfaces.calIGoogleSessionManager);
    }
    return this.mObject;
}

/**
 * setCalendarPref
 * Helper to set an independant Calendar Preference, since I cannot use the
 * calendar manager because of early initialization Problems.
 *
 * @param aCalendar     The Calendar to set the pref for
 * @param aPrefName     The Preference name
 * @param aPrefType     The type of the preference ("BOOL", "INT", "CHAR")
 * @param aPrefValue    The Preference value
 *
 * @return              The value of aPrefValue
 *
 * @require aCalendar.googleCalendarName
 */
function setCalendarPref(aCalendar, aPrefName, aPrefType, aPrefValue) {

    cal.setPref("calendar.google.calPrefs." + aCalendar.googleCalendarName + "." +
                aPrefName, aPrefValue, aPrefType);

    return aPrefValue;
}

/**
 * getCalendarPref
 * Helper to get an independant Calendar Preference, since I cannot use the
 * calendar manager because of early initialization Problems.
 *
 * @param aCalendar     The calendar to set the pref for
 * @param aPrefName     The preference name
 *
 * @return              The preference value
 *
 * @require aCalendar.googleCalendarName
 */
function getCalendarPref(aCalendar, aPrefName) {
    return cal.getPrefSafe("calendar.google.calPrefs." +
                           aCalendar.googleCalendarName + "."  + aPrefName);
}

/**
 * getFormattedString
 * Returns the string from the properties file, formatted with args
 *
 * @param aBundleName   The .properties file to access
 * @param aStringName   The property to access
 * @param aFormatArgs   An array of arguments to format the string
 * @param aComponent    Optionally, the stringbundle component name
 * @return              The formatted string
 */
function getFormattedString(aBundleName, aStringName, aFormatArgs, aComponent) {
    return calGetString(aBundleName, aStringName, aFormatArgs, aComponent || "gdata-provider");
}

/**
 * getCalendarCredentials
 * Tries to get the username/password combination of a specific calendar name
 * from the password manager or asks the user.
 *
 * @param   in  aCalendarName   The calendar name to look up. Can be null.
 * @param   out aUsername       The username that belongs to the calendar.
 * @param   out aPassword       The password that belongs to the calendar.
 * @param   out aSavePassword   Should the password be saved?
 * @return  Could a password be retrieved?
 */
function getCalendarCredentials(aCalendarName,
                                aUsername,
                                aPassword,
                                aSavePassword) {
    return cal.auth.getCredentials(getFormattedString("gdata", "loginDialogTitle"),
                                   aCalendarName,
                                   aUsername,
                                   aPassword,
                                   aSavePassword);
}

/**
 * Gets the date and time that Google's http server last sent us. Note the
 * passed argument is modified. This might not be the exact server time (i.e it
 * may be off by network latency), but it does give a good guess when syncing.
 *
 * @param aDate     The date to modify
 */
function getCorrectedDate(aDate) {

    if (!getCorrectedDate.mClockSkew) {
        return aDate;
    }

    aDate.second += getCorrectedDate.mClockSkew;
    return aDate;
}

/**
 * The timezone service to translate Google timezones.
 */
var gdataTimezoneService = {
    ctz: cal.getTimezoneService(),

    get floating() {
        return this.ctz.floating;
    },

    get UTC() {
        return this.ctz.UTC;
    },

    get version() {
        return this.ctz.version;
    },

    get defaultTimezone() {
        return this.ctz.defaultTimezone;
    },

    getTimezone: function gTS_getTimezone(aTzid) {
        if (aTzid == "Etc/GMT") {
            // Most timezones are covered by the timezone service, there is one
            // exception I've found out about. GMT without DST is pretty close
            // to UTC, lets take it.
            return UTC();
        }

        let baseTZ = this.ctz.getTimezone(aTzid);
        ASSERT(baseTZ, "Unknown Timezone requested: " + aTzid);
        return baseTZ;
    }
};

/**
 * passwordManagerSave
 * Helper to insert an entry to the password manager.
 *
 * @param aUserName     The username to search
 * @param aPassword     The corresponding password
 */
function passwordManagerSave(aUsername, aPassword) {
    cal.auth.passwordManagerSave(aUsername, aPassword, aUsername, "Google Calendar");
}

/**
 * passwordManagerGet
 * Helper to retrieve an entry from the password manager
 *
 * @param in  aUsername     The username to search
 * @param out aPassword     The corresponding password
 * @return                  Does an entry exist in the password manager
 */
function passwordManagerGet(aUsername, aPassword) {
    return cal.auth.passwordManagerGet(aUsername, aPassword, aUsername, "Google Calendar");
}

/**
 * passwordManagerRemove
 * Helper to remove an entry from the password manager
 *
 * @param aUsername     The username to remove.
 * @return              Could the user be removed?
 */
function passwordManagerRemove(aUsername) {
    return cal.auth.passwordManagerRemove(aUsername, aUsername, "Google Calendar");
}

/**
 * If the operation has signaled that a conflict occurred, then prompt the user
 * to overwrite. If the user chooses to overwrite, restart the request with the
 * right parameters so the request succeeds.
 *
 * @param aOperation        The operation to check
 * @param aItem             The updated item from the response
 * @return                  False if further processing should be cancelled
 */
function resolveConflicts(aOperation, aItem) {
    if (aItem && (aOperation.status == kGOOGLE_CONFLICT_DELETED ||
                  aOperation.status == kGOOGLE_CONFLICT_MODIFY)) {
        if (aItem == "SEQUENCE-HACK") {
            // Working around a Google issue here, see what happens on a 400
            // code in calGoogleRequest.js. This will cause a new request
            // without the sequence number. In return, we get a new item with
            // the correct sequence number.
            let newItem =  aOperation.newItem.clone();
            let session = aOperation.calendar.session;
            newItem.deleteProperty("SEQUENCE");
            let xmlEntry = ItemToXMLEntry(newItem, aOperation.calendar,
                                          session.userName, session.fullName);

            aOperation.newItem = newItem;
            aOperation.setUploadData("application/atom+xml; charset=UTF-8", cal.xml.serializeDOM(xmlEntry));
            session.asyncItemRequest(aOperation);
            return false;
        } else if (aOperation.status == kGOOGLE_CONFLICT_DELETED &&
                   aOperation.type == aOperation.DELETE) {
            // Deleted on the server and deleted locally. Great!
            return true;
        } else {
            // If a conflict occurred, then prompt
            let method = (aOperation.type == aOperation.DELETE ? "delete" : "modify")
            let inputItem = aOperation.oldItem || aOperation.newItem;
            let overwrite = cal.promptOverwrite(method, inputItem);
            if (overwrite) {
                if (aOperation.status == kGOOGLE_CONFLICT_DELETED &&
                    aOperation.type == aOperation.MODIFY) {
                    // The item was deleted on the server, but modified locally.
                    // Add it again
                    aOperation.type = aOperation.ADD;
                    aOperation.uri = aOperation.calendar.fullUri.spec;
                    aOperation.calendar.session.asyncItemRequest(aOperation);
                    return false;
                } else if (aOperation.status == kGOOGLE_CONFLICT_MODIFY &&
                           aOperation.type == aOperation.MODIFY) {
                    // The item was modified in both places, repeat the current
                    // request with the edit uri of the updated event
                    aOperation.uri = getItemEditURI(aItem);
                    aOperation.calendar.session.asyncItemRequest(aOperation);
                    return false;
                } else if (aOperation.status == kGOOGLE_CONFLICT_MODIFY &&
                           aOperation.type == aOperation.DELETE) {
                    // Modified on the server, deleted locally. Just repeat the
                    // delete request with the updated edit uri.
                    aOperation.uri = getItemEditURI(aItem);
                    aOperation.calendar.session.asyncItemRequest(aOperation);
                    return false;
                }
            }
        }
        // Otherwise, we can just continue using the item that was parsed, it
        // is the newest version on the server.
    }
    return true;
}

/**
 * Helper function to convert raw data directly into a calIItemBase. If the
 * passed operation signals an error, then throw an exception
 *
 * @param aOperation        The operation to check for errors
 * @param aData             The result from the response
 * @param aGoogleCalendar   The calIGoogleCalendar to operate on
 * @param aReferenceItem    The reference item to apply the information to
 * @return                  The parsed item
 * @throws                  An exception on a parsing or request error
 */
function DataToItem(aOperation, aData, aGoogleCalendar, aReferenceItem) {
    if (aOperation.status == kGOOGLE_CONFLICT_DELETED ||
        aOperation.status == kGOOGLE_CONFLICT_MODIFY ||
        Components.isSuccessCode(aOperation.status)) {

        let item;
        if (aData == "SEQUENCE-HACK") {
            // Working around a Google issue here, see what happens on a 400
            // code in calGoogleRequest.js. This will be processed in
            // resolveConflicts().
            return "SEQUENCE-HACK";
        }

        if (aData && aData.length) {
            let xml = cal.xml.parseString(aData);
            cal.LOG("[calGoogleCalendar] Parsing entry:\n" + aData + "\n");

            // Get the local timezone from the preferences
            let timezone = calendarDefaultTimezone();

            // Parse the Item with the given timezone
            item = XMLEntryToItem(xml.documentElement, timezone,
                                  aGoogleCalendar,
                                  aReferenceItem);
        } else {
            cal.LOG("[calGoogleCalendar] No content, using reference item instead ");
            // No data happens for example on delete. Just assume the reference
            // item.
            item = aReferenceItem.clone();
        }

        LOGitem(item);
        item.calendar = aGoogleCalendar.superCalendar;
        return item;
    } else {
        throw new Components.Exception(aData, aOperation.status);
    }
}

/**
 * ItemToXMLEntry
 * Converts a calIEvent to a string of xml data.
 *
 * @param aItem         The item to convert
 * @param aCalendar     The calendar to use, this must be a calIGoogleCalendar
 * @param aAuthorEmail  The email of the author of the event
 * @param aAuthorName   The full name of the author of the event
 * @return              The xml data of the item
 */
function ItemToXMLEntry(aItem, aCalendar, aAuthorEmail, aAuthorName) {
    let selfIsOrganizer = (!aItem.organizer ||
                            aItem.organizer.id == "mailto:" + aCalendar.googleCalendarName);

    function addExtendedProperty(aName, aValue) {
        if (!selfIsOrganizer || !aValue) {
            // We can't set extended properties if we are not the organizer,
            // discard. Also, if the value is null/false, we can delete the
            // extended property by not adding it.
            return;
        }
        let gdExtendedProp = document.createElementNS(gdNS, "extendedProperty");
        gdExtendedProp.setAttribute("name", aName);
        gdExtendedProp.setAttribute("value", aValue || "");
        entry.appendChild(gdExtendedProp);
    }

    if (!aItem) {
        throw new Components.Exception("", Components.results.NS_ERROR_INVALID_ARG);
    }

    const kEVENT_SCHEMA = "http://schemas.google.com/g/2005#event.";

    // Document creation
    let document = cal.xml.parseString('<entry xmlns="' + atomNS + '" xmlns:gd="' + gdNS + '" xmlns:gCal="' + gcalNS + '"/>');
    let entry = document.documentElement;

    // Helper functions
    function elemNS(ns, name) document.createElementNS(ns, name);
    function addElemNS(ns, name, parent) (parent || entry).appendChild(elemNS(ns, name));

    // Basic elements
    let kindElement = addElemNS(atomNS, "category");
    kindElement.setAttribute("scheme", "http://schemas.google.com/g/2005#kind");
    kindElement.setAttribute("term", "http://schemas.google.com/g/2005#event");

    let titleElement = addElemNS(atomNS, "title");
    titleElement.setAttribute("type", "text");
    titleElement.textContent = aItem.title;

    // atom:content
    let contentElement = addElemNS(atomNS, "content");
    contentElement.setAttribute("type", "text");
    contentElement.textContent = aItem.getProperty("DESCRIPTION") || "";

    // atom:author
    let authorElement = addElemNS(atomNS, "author");
    addElemNS(atomNS, "name", authorElement).textContent = aAuthorName || aAuthorEmail;
    addElemNS(atomNS, "email", authorElement).textContent = aAuthorEmail;

    // gd:transparency
    let transpElement = addElemNS(gdNS, "transparency");
    let transpValue = aItem.getProperty("TRANSP") || "opaque";
    transpElement.setAttribute("value", kEVENT_SCHEMA + transpValue.toLowerCase());

    // gd:eventStatus
    let status = aItem.status || "confirmed";
    if (status == "CANCELLED") {
        // If the status is canceled, then the event will be deleted. Since the
        // user didn't choose to delete the event, we will protect him and not
        // allow this status to be set
        throw new Components.Exception("",
                                       Components.results.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA);
    } else if (status == "NONE") {
        status = "CONFIRMED";
    }
    addElemNS(gdNS, "eventStatus").setAttribute("value", kEVENT_SCHEMA + status.toLowerCase());

    // gd:where
    addElemNS(gdNS, "where").setAttribute("valueString", aItem.getProperty("LOCATION") || "");

    // gd:who
    if (cal.getPrefSafe("calendar.google.enableAttendees", false)) {
        // XXX Only parse attendees if they are enabled, due to bug 407961

        let attendees = aItem.getAttendees({});
        if (aItem.organizer) {
            // Taking care of the organizer is the same as taking care of any other
            // attendee. Add the organizer to the local attendees list.
            attendees.push(aItem.organizer);
        }

        const attendeeStatusMap = {
            "REQ-PARTICIPANT": "required",
            "OPT-PARTICIPANT": "optional",
            "NON-PARTICIPANT": null,
            "CHAIR": null,

            "NEEDS-ACTION": "invited",
            "ACCEPTED": "accepted",
            "DECLINED": "declined",
            "TENTATIVE": "tentative",
            "DELEGATED": "tentative"
        };

        for each (let attendee in attendees) {
            if (attendee.userType && attendee.userType != "INDIVIDUAL") {
                // We can only take care of individuals.
                continue;
            }

            let xmlAttendee = addElemNS(gdNS, "who");

            // Strip "mailto:" part
            xmlAttendee.setAttribute("email", attendee.id.replace(/^mailto:/, ""));

            if (attendee.isOrganizer) {
                xmlAttendee.setAttribute("rel", kEVENT_SCHEMA + "organizer");
            } else {
                xmlAttendee.setAttribute("rel", kEVENT_SCHEMA + "attendee");
            }

            if (attendee.commonName) {
                xmlAttendee.setAttribute("valueString", attendee.commonName);
            }

            if (attendeeStatusMap[attendee.role]) {
                let attendeeTypeElement = addElemNS(gdNS, "attendeeType", xmlAttendee);
                let attendeeTypeValue = kEVENT_SCHEMA + attendeeStatusMap[attendee.role];
                attendeeTypeElement.setAttribute("value", attendeeTypeValue);
            }

            if (attendeeStatusMap[attendee.participationStatus]) {
                let attendeeStatusElement = addElemNS(gdNS, "attendeeStatus", xmlAttendee);
                let attendeeStatusValue = kEVENT_SCHEMA + attendeeStatusMap[attendee.participationStatus];
                attendeeStatusElement.setAttribute("value", attendeeStatusValue);
            }
        }
    }

    // Don't notify attendees by default. Use a preference in case the user
    // wants this to be turned on.
    let notify = cal.getPrefSafe("calendar.google.sendEventNotifications", false);
    addElemNS(gcalNS, "sendEventNotifications").setAttribute("value", notify ? "true" : "false");

    // gd:when
    let duration = aItem.endDate.subtractDate(aItem.startDate);
    let whenElement;
    if (!aItem.recurrenceInfo) {
        // gd:when isn't allowed for recurring items where gd:recurrence is set
        whenElement = addElemNS(gdNS, "when");
        whenElement.setAttribute("startTime", cal.toRFC3339(aItem.startDate));
        whenElement.setAttribute("endTime", cal.toRFC3339(aItem.endDate));
    }

    // gd:reminder
    let alarms = aItem.getAlarms({});
    let actionMap = {
        DISPLAY: "alert",
        EMAIL: "email",
        SMS: "sms"
    };
    if (selfIsOrganizer) {
        for (let i = 0; i < 5 && i < alarms.length; i++) {
            let alarm = alarms[i];
            let gdReminder;
            if (aItem.recurrenceInfo) {
                // On recurring items, set the reminder directly in the <entry> tag.
                gdReminder = addElemNS(gdNS, "reminder");
            } else {
                // Otherwise, its a child of the gd:when element
                gdReminder = addElemNS(gdNS, "reminder", whenElement);
            }
            if (alarm.related == alarm.ALARM_RELATED_ABSOLUTE) {
                // Setting an absolute date can be done directly. Google will take
                // care of calculating the offset.
                gdReminder.setAttribute("absoluteTime", cal.toRFC3339(alarm.alarmDate));
            } else {
                let alarmOffset = alarm.offset;
                if (alarm.related == alarm.ALARM_RELATED_END) {
                    // Google always uses an alarm offset related to the start time
                    // for relative alarms.
                    alarmOffset = alarmOffset.clone();
                    alarmOffset.addDuration(duration);
                }

                gdReminder.setAttribute("minutes", -alarmOffset.inSeconds / 60);
                gdReminder.setAttribute("method", actionMap[alarm.action] || "alert");
            }
        }
    } else if (alarms.length) {
        // We need to reset this so the item gets returned correctly.
        aItem.clearAlarms();
    }

    // gd:extendedProperty (alarmLastAck)
    addExtendedProperty("X-MOZ-LASTACK", cal.toRFC3339(aItem.alarmLastAck));

    // XXX While Google now supports multiple alarms and alarm values, we still
    // need to fix bug 353492 first so we can better take care of finding out
    // what alarm is used for snoozing.

    // gd:extendedProperty (snooze time)
    let itemSnoozeTime = aItem.getProperty("X-MOZ-SNOOZE-TIME");
    let icalSnoozeTime = null;
    if (itemSnoozeTime) {
        // The propery is saved as a string, translate back to calIDateTime.
        icalSnoozeTime = cal.createDateTime();
        icalSnoozeTime.icalString = itemSnoozeTime;
    }
    addExtendedProperty("X-MOZ-SNOOZE-TIME", cal.toRFC3339(icalSnoozeTime));

    // gd:extendedProperty (snooze recurring alarms)
    let snoozeValue = "";
    if (aItem.recurrenceInfo) {
        // This is an evil workaround since we don't have a really good system
        // to save the snooze time for recurring alarms or even retrieve them
        // from the event. This should change when we have multiple alarms
        // support.
        let snoozeObj = {};
        let enumerator = aItem.propertyEnumerator;
        while (enumerator.hasMoreElements()) {
            let prop = enumerator.getNext().QueryInterface(Components.interfaces.nsIProperty);
            if (prop.name.substr(0, 18) == "X-MOZ-SNOOZE-TIME-") {
                // We have a snooze time for a recurring event, add it to our object
                snoozeObj[prop.name.substr(18)] = prop.value;
            }
        }
        snoozeValue = JSON.stringify(snoozeObj);
    }
    // Now save the snooze object in source format as an extended property. Do
    // so always, since its currently impossible to unset extended properties.
    addExtendedProperty("X-GOOGLE-SNOOZE-RECUR", snoozeValue);

    // gd:visibility
    let privacy = aItem.privacy || "default";
    addElemNS(gdNS, "visibility").setAttribute("value", kEVENT_SCHEMA + privacy.toLowerCase());

    // categories
    // Google does not support categories natively, but allows us to store data
    // as an "extendedProperty", so we do here
    addExtendedProperty("X-MOZ-CATEGORIES",
                        categoriesArrayToString(aItem.getCategories({})));

    // gd:recurrence
    if (aItem.recurrenceInfo) {
        try {
            const kNEWLINE = "\r\n";
            let icalString;
            let recurrenceItems = aItem.recurrenceInfo.getRecurrenceItems({});

            // Dates of the master event
            let startTZID = aItem.startDate.timezone.tzid;
            let endTZID = aItem.endDate.timezone.tzid;
            icalString = "DTSTART;TZID=" + startTZID
                         + ":" + aItem.startDate.icalString + kNEWLINE
                         + "DTEND;TZID=" + endTZID
                         + ":"  + aItem.endDate.icalString + kNEWLINE;

            // Add all recurrence items to the ical string
            for each (let ritem in recurrenceItems) {
                let prop = ritem.icalProperty;
                let wrappedRItem = cal.wrapInstance(wrappedRItem, Components.interfaces.calIRecurrenceDate);
                if (wrappedRItem) {
                    // EXDATES require special casing, since they might contain
                    // a TZID. To avoid the need for conversion of TZID strings,
                    // convert to UTC before serialization.
                    prop.valueAsDatetime = wrappedRItem.date.getInTimezone(cal.UTC());
                }
                icalString += prop.icalString;
            }

            // Put the ical string in a <gd:recurrence> tag
            addElemNS(gdNS, "recurrence").textContent = icalString + kNEWLINE;
        } catch (e) {
            cal.ERROR("[calGoogleCalendar] Error: " + e);
        }
    }

    // gd:originalEvent
    if (aItem.recurrenceId) {
        let originalEvent = addElemNS(gdNS, "originalEvent");
        originalEvent.setAttribute("id", aItem.parentItem.id);

        let origWhen = addElemNS(gdNS, "when", originalEvent)
        origWhen.setAttribute("startTime", cal.toRFC3339(aItem.recurrenceId.getInTimezone(cal.UTC())));
    }

    // While it may sometimes not work out, we can always try to set the uid and
    // sequence properties
    let sequence = aItem.getProperty("SEQUENCE");
    if (sequence) {
        addElemNS(gcalNS, "sequence").setAttribute("value", sequence);
    }
    addElemNS(gcalNS, "uid").setAttribute("value", aItem.id || "");

    // XXX Google currently has no priority support. See
    // http://code.google.com/p/google-gdata/issues/detail?id=52
    // for details.

    return document;
}

/**
 * relevantFieldsMatch
 * Tests if all google supported fields match
 *
 * @param a The reference item
 * @param b The comparing item
 * @return  true if all relevant fields match, otherwise false
 */
function relevantFieldsMatch(a, b) {

    // flat values
    if (a.id != b.id ||
        a.title != b.title ||
        a.status != b.status ||
        a.privacy != b.privacy) {
        return false;
    }

    function compareNotNull(prop) {
        let ap = a[prop];
        let bp = b[prop];
        return (ap && !bp || !ap && bp ||
                (typeof(ap) == 'object' && ap && bp &&
                 ap.compare && ap.compare(bp)));
    }

    // Object flat values
    if (compareNotNull("recurrenceInfo") ||
        compareNotNull("alarmLastAck") ||
        /* Compare startDate and endDate */
        compareNotNull("startDate") ||
        compareNotNull("endDate") ||
        (a.startDate.isDate != b.startDate.isDate) ||
        (a.endDate.isDate != b.endDate.isDate)) {
        return false;
    }

    // Properties
    const kPROPERTIES = ["DESCRIPTION", "TRANSP", "X-GOOGLE-EDITURL",
                         "LOCATION", "X-MOZ-SNOOZE-TIME"];

    for each (let p in kPROPERTIES) {
        // null and an empty string should be handled as non-relevant
        if ((a.getProperty(p) || "") != (b.getProperty(p) || "")) {
            return false;
        }
    }

    // categories
    let aCat = a.getCategories({});
    let bCat = b.getCategories({});
    if ((aCat.length != bCat.length) ||
        aCat.some(function notIn(cat) { return (bCat.indexOf(cat) == -1); })) {
        return false;
    }

    // attendees and organzier
    let aa = a.getAttendees({});
    let ab = b.getAttendees({});
    if (aa.length != ab.length) {
        return false;
    }

    if ((a.organizer && !b.organizer) ||
        (!a.organizer && b.organizer) ||
        (a.organizer && b.organizer && a.organizer.id != b.organizer.id)) {
        return false;
    }

    // go through attendees in a, check if its id is in b
    for each (let attendee in aa) {
        let ba = b.getAttendeeById(attendee.id);
        if (!ba ||
            ba.participationStatus != attendee.participationStatus ||
            ba.commonName != attendee.commonName ||
            ba.isOrganizer != attendee.isOrganizer ||
            ba.role != attendee.role) {
            return false;
        }
    }

    // Alarms
    aa = a.getAlarms({});
    ab = b.getAlarms({});

    if (aa.length != ab.length) {
        return false;
    }

    let alarmMap = {};
    for each (let alarm in aa) {
        alarmMap[alarm.icalString] = true;
    }
    let found = 0;
    for each (let alarm in ab) {
        if (alarm.icalString in alarmMap) {
            found++;
        }
    }

    if (found != ab.length) {
        return false;
    }

    // Recurrence Items
    if (a.recurrenceInfo) {
        let ra = a.recurrenceInfo.getRecurrenceItems({});
        let rb = b.recurrenceInfo.getRecurrenceItems({});

        // If we have more or less, it definitly changed.
        if (ra.length != rb.length) {
            return false;
        }

        // I assume that if the recurrence pattern has not changed, the order
        // of the recurrence items should not change. Anything more will be
        // very expensive.
        for (let i = 0; i < ra.length; i++) {
            if (ra[i].icalProperty.icalString !=
                rb[i].icalProperty.icalString) {
                return false;
            }
        }
    }

    return true;
}

/**
 * getItemEditURI
 * Helper to get the item's edit URI
 *
 * @param aItem         The item to get it from
 * @return              The edit URI
 */
function getItemEditURI(aItem) {

    ASSERT(aItem);
    let edituri = aItem.getProperty("X-GOOGLE-EDITURL");
    if (!edituri) {
        // If the item has no edit uri, it is read-only
        throw new Components.Exception("The item is readonly", Components.interfaces.calIErrors.CAL_IS_READONLY);
    }
    return edituri;
}

function getIdFromEntry(aXMLEntry) {
    let id = gdataXPathFirst(aXMLEntry, 'gCal:uid/@value');
    return id.replace(/@google.com/,"");
}

function getRecurrenceIdFromEntry(aXMLEntry, aTimezone) {
    let rId = gdataXPathFirst(aXMLEntry, 'gd:originalEvent/gd:when/@startTime');
    return (rId ? cal.fromRFC3339(rId.toString(), aTimezone) : null);
}

/**
 * XMLEntryToItem
 * Converts a string of xml data to a calIEvent.
 *
 * @param aXMLEntry         The xml data of the item
 * @param aTimezone         The timezone the event is most likely in
 * @param aCalendar         The calendar this item will belong to. This needs to
 *                              be a calIGoogleCalendar instance.
 * @param aReferenceItem    The item to apply the information from the xml to.
 *                              If null, a new item will be used.
 * @return                  The calIEvent with the item data.
 */
function XMLEntryToItem(aXMLEntry, aTimezone, aCalendar, aReferenceItem) {

    function getExtendedProperty(x) gdataXPathFirst(aXMLEntry, 'gd:extendedProperty[@name="' + x + '"]/@value');

    if (!aXMLEntry) {
        throw new Components.Exception("", Components.results.NS_ERROR_FAILURE);
    } else if (typeof aXMLEntry == "string") {
        aXMLEntry = cal.xml.parseString(aXMLEntry);
    }

    let item = (aReferenceItem ? aReferenceItem.clone() : cal.createEvent());

    try {
        // id
        item.id = getIdFromEntry(aXMLEntry);

        // sequence
        item.setProperty("SEQUENCE", gdataXPathFirst(aXMLEntry, 'gCal:sequence/@value') || 0);

        // link (edit url)
        // Since Google doesn't set the edit url to be https if the request is
        // https, we need to work around this here.
        let editUrl = gdataXPathFirst(aXMLEntry, 'atom:link[@rel="edit"]/@href');
        if (aCalendar.uri.schemeIs("https")) {
            editUrl = editUrl.replace(/^http:/, "https:");
        }
        item.setProperty("X-GOOGLE-EDITURL", editUrl);

        // link (alternative representation, html)
        let htmlUrl = gdataXPathFirst(aXMLEntry, 'atom:link[@rel="alternate"]/@href');
        if (aCalendar.uri.schemeIs("https")) {
            htmlUrl = htmlUrl.replace(/^http:/, "https:");
        }
        item.setProperty("URL", htmlUrl);

        // title
        item.title = gdataXPathFirst(aXMLEntry, 'atom:title[@type="text"]/text()');

        // content
        item.setProperty("DESCRIPTION", gdataXPathFirst(aXMLEntry, 'atom:content[@type="text"]/text()'));

        // gd:transparency
        item.setProperty("TRANSP", gdataXPathFirst(aXMLEntry, 'gd:transparency/@value').substring(39).toUpperCase());

        // gd:eventStatus
        item.status = gdataXPathFirst(aXMLEntry, 'gd:eventStatus/@value').substring(39).toUpperCase();

        // gd:reminder (preparation)
        // If a reference item was passed, it may already contain alarms. Since
        // we have no alarm id or such and the alarms are contained in every
        // feed, we can go ahead and clear the alarms here.
        item.clearAlarms();

        /**
         * Helper function to parse all reminders in a tagset.
         *
         * @param reminderTags      The tagset to parse.
         */
        function parseReminders(reminderTags) {
            let organizerEmail = gdataXPathFirst(aXMLEntry, 'gd:who[@rel="http://schemas.google.com/g/2005#event.organizer"]/@email');
            if (organizerEmail != aCalendar.googleCalendarName) {
                // We are not the organizer, so its not smart to set alarms on
                // this event.
                return;
            }
            const actionMap = {
                email: "EMAIL",
                alert: "DISPLAY",
                sms: "SMS"
            };
            for each (let reminderTag in reminderTags) {
                let alarm = cal.createAlarm();
                alarm.action = actionMap[reminderTag.getAttribute("method")] || "DISPLAY";

                let absoluteTime = reminderTag.getAttribute("absoluteTime");
                if (absoluteTime) {
                    alarm.related = Components.interfaces.calIAlarm.ALARM_RELATED_ABSOLUTE;
                    alarm.alarmDate = cal.fromRFC3339(absoluteTime, aTimezone);
                } else {
                    alarm.related = Components.interfaces.calIAlarm.ALARM_RELATED_START;
                    let alarmOffset = cal.createDuration();
                    let days = reminderTag.getAttribute("days");
                    let hours = reminderTag.getAttribute("hours");
                    let minutes = reminderTag.getAttribute("minutes");

                    if (days) {
                        alarmOffset.days = -days;
                    } else if (hours) {
                        alarmOffset.hours = -hours;
                    } else if (minutes) {
                        alarmOffset.minutes = -minutes;
                    } else {
                        // Invalid alarm, skip it
                        continue;
                    }
                    alarmOffset.normalize();
                    alarm.offset = alarmOffset;
                }
                item.addAlarm(alarm);
            }
        }

        // gd:when
        let recurrenceInfo = gdataXPathFirst(aXMLEntry, 'gd:recurrence/text()');
        if (!recurrenceInfo || recurrenceInfo.length == 0) {
            // If no recurrence information is given, then there will only be
            // one gd:when tag. Otherwise, we will be parsing the startDate from
            // the recurrence information.
            item.startDate = cal.fromRFC3339(gdataXPathFirst(aXMLEntry, 'gd:when/@startTime'), aTimezone);
            item.endDate = cal.fromRFC3339(gdataXPathFirst(aXMLEntry, 'gd:when/@endTime'), aTimezone);

            if (!item.endDate) {
                // We have a zero-duration event
                item.endDate = item.startDate.clone();
            }

            // gd:reminder
            parseReminders(gdataXPath(aXMLEntry, 'gd:when/gd:reminder'));
        } else {
            if (!item.recurrenceInfo) {
                item.recurrenceInfo = cal.createRecurrenceInfo(item);
            } else {
                item.recurrenceInfo.clearRecurrenceItems();
            }

            // We don't really care about google's timezone info for
            // now. This may change when bug 314339 is fixed. Split out
            // the timezone information so we only have the first bit
            let vevent = recurrenceInfo;
            let splitpos = recurrenceInfo.indexOf("BEGIN:VTIMEZONE");
            if (splitpos > -1) {
                // Sometimes (i.e if only DATE values are specified), no
                // timezone info is contained. Only remove it if it shows up.
                vevent = recurrenceInfo.substring(0, splitpos);
            }

            vevent = "BEGIN:VEVENT\n" + vevent + "END:VEVENT";
            let icsService = cal.getIcsService();

            let rootComp = icsService.parseICS(vevent, gdataTimezoneService);
            let i = 0;
            let hasRecurringRules = false;
            for (let prop in cal.ical.propertyIterator(rootComp)) {
               switch (prop.propertyName) {
                    case "EXDATE":
                        let recItem = Components.classes["@mozilla.org/calendar/recurrence-date;1"]
                                      .createInstance(Components.interfaces.calIRecurrenceDate);
                        try {
                            recItem.icalProperty = prop;
                            item.recurrenceInfo.appendRecurrenceItem(recItem);
                            hasRecurringRules = true;
                        } catch (e) {
                            Components.utils.reportError(e);
                        }
                        break;
                    case "RRULE":
                        let recRule = cal.createRecurrenceRule();
                        try {
                            recRule.icalProperty = prop;
                            item.recurrenceInfo.appendRecurrenceItem(recRule);
                            hasRecurringRules = true;
                        } catch (e) {
                            Components.utils.reportError(e);
                        }
                        break;
                    case "DTSTART":
                        item.startDate = prop.valueAsDatetime;
                        break;
                    case "DTEND":
                        item.endDate = prop.valueAsDatetime;
                        break;
                }
            }

            if (!hasRecurringRules) {
                // Sometimes Google gives us events that have <gd:recurrence>
                // but contain no recurrence rules. Treat the event as a normal
                // event. See gdata issue 353.
                item.recurrenceInfo = null;
            }

            // gd:reminder (for recurring events)
            // This element is supplied as a direct child to the <entry> element
            // for recurring items.
            parseReminders(gdataXPath(aXMLEntry, 'gd:reminder'));
        }

        // gd:recurrenceException
        let exceptions = gdataXPath(aXMLEntry, 'gd:recurrenceException[@specialized="true"]/gd:entryLink/atom:entry');
        for each (let exception in exceptions) {
            // We only want specialized exceptions, mainly becuase I haven't
            // quite found out if a non-specialized exception also corresponds
            // to a normal exception as libical knows it.
            let excItem = XMLEntryToItem(exception, aTimezone, aCalendar);

            // Google uses the status field to reflect negative exceptions.
            if (excItem.status == "CANCELED") {
                item.recurrenceInfo.removeOccurrenceAt(excItem.recurrenceId);
            } else {
                excItem.calendar = aCalendar.superCalendar;
                item.recurrenceInfo.modifyException(excItem, true);
            }
        }

        // gd:extendedProperty (alarmLastAck)
        item.alarmLastAck = cal.fromRFC3339(getExtendedProperty("X-MOZ-LASTACK"), aTimezone);

        // gd:extendedProperty (snooze time)
        let dtSnoozeTime = cal.fromRFC3339(getExtendedProperty("X-MOZ-SNOOZE-TIME"), aTimezone);
        let snoozeProperty = (dtSnoozeTime ? dtSnoozeTime.icalString : null);
        item.setProperty("X-MOZ-SNOOZE-TIME", snoozeProperty);

        // gd:extendedProperty (snooze recurring alarms)
        if (item.recurrenceInfo) {
            // Transform back the string into our snooze properties
            let snoozeObj;
            try {
                let snoozeString = getExtendedProperty("X-GOOGLE-SNOOZE-RECUR");
                snoozeObj = JSON.parse(snoozeString);
            } catch (e) {
                // Just swallow parsing errors, not so important.
            }

            if (snoozeObj) {
                for (let rid in snoozeObj) {
                    item.setProperty("X-MOZ-SNOOZE-TIME-" + rid,
                                     snoozeObj[rid]);
                }
            }
        }

        // gd:where
        item.setProperty("LOCATION", gdataXPathFirst(aXMLEntry, 'gd:where/@valueString'));

        // gd:who
        if (cal.getPrefSafe("calendar.google.enableAttendees", false)) {
            // XXX Only parse attendees if they are enabled, due to bug 407961

            // This object can easily translate Google's values to our values.
            const attendeeStatusMap = {
                // role
                "event.optional": "OPT-PARTICIPANT",
                "event.required": "REQ-PARTICIPANT",

                // Participation Statii
                "event.accepted": "ACCEPTED",
                "event.declined": "DECLINED",
                "event.invited": "NEEDS-ACTION",
                "event.tentative": "TENTATIVE"
            };

            // Clear all attendees in case a reference item was passed
            item.removeAllAttendees();

            // Iterate all attendee tags.
            for each (let who in gdataXPath(aXMLEntry, 'gd:who')) {
                let attendee = cal.createAttendee();
                let rel = who.getAttribute("rel").substring(33);
                let type = gdataXPathFirst(who, 'gd:attendeeType/@value').substring(33);
                let status = gdataXPathFirst(who, 'gd:attendeeStatus/@value').substring(33);

                attendee.id = "mailto:" + who.getAttribute("email")
                attendee.commonName = who.getAttribute("valueString");
                attendee.rsvp = "FALSE";
                attendee.userType = "INDIVIDUAL";
                attendee.isOrganizer = (rel == "event.organizer");
                attendee.participationStatus = attendeeStatusMap[status];
                attendee.role = attendeeStatusMap[type]
                attendee.makeImmutable();

                if (attendee.isOrganizer) {
                    item.organizer = attendee;
                } else {
                    item.addAttendee(attendee);
                }
            }
        }

        // gd:originalEvent
        item.recurrenceId = getRecurrenceIdFromEntry(aXMLEntry, aTimezone);

        // gd:visibility
        item.privacy = gdataXPathFirst(aXMLEntry, "gd:visibility/@value").substring(39).toUpperCase();

        // category
        // Google does not support categories natively, but allows us to store
        // data as an "extendedProperty", and here it's going to be retrieved
        // again
        let categories = cal.categoriesStringToArray(getExtendedProperty("X-MOZ-CATEGORIES"));
        item.setCategories(categories.length, categories);

        // published
        let createdText = gdataXPathFirst(aXMLEntry, 'atom:published/text()');
        item.setProperty("CREATED", cal.fromRFC3339(createdText, aTimezone).getInTimezone(cal.UTC()));

        // updated (This must be set last!)
        let lastmodText = gdataXPathFirst(aXMLEntry, 'atom:updated/text()');
        item.setProperty("LAST-MODIFIED", cal.fromRFC3339(lastmodText, aTimezone).getInTimezone(cal.UTC()));

        // XXX Google currently has no priority support. See
        // http://code.google.com/p/google-gdata/issues/detail?id=52
        // for details.
    } catch (e) {
        cal.ERROR("Error parsing XML stream" + e);
        throw e;
    }
    return item;
}

/**
 * Expand an item to occurrences, if the operation's item filter requests it.
 * Otherwise returns the item in an array.
 *
 * @param aItem         The item to expand
 * @param aOperation    The calIGoogleRequest that contains the filter and
 *                        ranges.
 * @return              The (possibly expanded) items in an array.
 */
function expandItems(aItem, aOperation) {
    let expandedItems;
    if (aOperation.itemFilter &
        Components.interfaces.calICalendar.ITEM_FILTER_CLASS_OCCURRENCES) {
        expandedItems = aItem.getOccurrencesBetween(aOperation.itemRangeStart,
                                                    aOperation.itemRangeEnd,
                                                    {});
        cal.LOG("[calGoogleCalendar] Expanded item " + aItem.title + " to " +
                expandedItems.length + " items");
    }
    return expandedItems || [aItem];
}

/**
 * Returns true if the exception passed is one that should cause the cache
 * layer to retry the operation. This is usually a network error or other
 * temporary error
 *
 * @param e     The exception to check
 */
function isCacheException(e) {
    // Stolen from nserror.h
    const NS_ERROR_MODULE_NETWORK = 6;
    function NS_ERROR_GET_MODULE(code) {
        return (((code >> 16) - 0x45) & 0x1fff);
    }

    if (NS_ERROR_GET_MODULE(e.result) == NS_ERROR_MODULE_NETWORK &&
        !Components.isSuccessCode(e.result)) {
        // This is a network error, which most likely means we should
        // retry it some time.
        return true;
    }

    // Other potential errors we want to retry with
    switch (e.result) {
        case Components.results.NS_ERROR_NOT_AVAILABLE:
            return true;
        default:
            return false;
    }
}

/**
 * Helper prototype to set a certain variable to the first item passed via get
 * listener. Cleans up code.
 */
function syncSetter(aObj) {
    this.mObj = aObj
}
syncSetter.prototype = {

    onGetResult: function syncSetter_onGetResult(aCal,
                                                 aStatus,
                                                 aIID,
                                                 aDetail,
                                                 aCount,
                                                 aItems) {
        this.mObj.value = aItems[0];
    },

    onOperationComplete: function syncSetter_onOperationComplete(aCal,
                                                                 aStatus,
                                                                 aOpType,
                                                                 aId,
                                                                 aDetail) {

        if (!Components.isSuccessCode(aStatus)) {
            this.mObj.value = null;
        }
    }
};

/**
 * Helper function to create a timer in a context where window.setTimeout is not
 * available
 *
 * @param aFunc     The function to call when the timer fires
 * @param aTimeout  The timeout in milliseconds.
 * @param aThis     (optional) The |this| object to call the function with.
 */
function setTimeout(aFunc, aTimeout, aThis) {
    let timerCallback = {
        notify: function setTimeout_notify() {
            aFunc.call(aThis);
        }
    };
    let timer = Components.classes["@mozilla.org/timer;1"]
                          .createInstance(Components.interfaces.nsITimer);
    timer.initWithCallback(timerCallback, aTimeout, timer.TYPE_ONE_SHOT);
}

/**
 * LOGitem
 * Custom logging functions
 */
function LOGitem(item) {
    if (!item) {
        return;
    }

    let attendees = item.getAttendees({});
    let attendeeString = "";
    for each (let a in attendees) {
        attendeeString += "\n" + LOGattendee(a);
    }

    let rstr = "\n";
    if (item.recurrenceInfo) {
        let ritems = item.recurrenceInfo.getRecurrenceItems({});
        for each (let ritem in ritems) {
            rstr += "\t\t" + ritem.icalProperty.icalString;
        }

        rstr += "\tExceptions:\n";
        let exids = item.recurrenceInfo.getExceptionIds({});
        for each (let exc in exids) {
            rstr += "\t\t" + exc + "\n";
        }
    }

    let astr = "\n";
    let alarms = item.getAlarms({});
    for each (let alarm in alarms) {
        astr += "\t\t" + LOGalarm(alarm) + "\n";
    }

    cal.LOG("[calGoogleCalendar] Logging calIEvent:" +
        "\n\tid:" + item.id +
        "\n\tediturl:" + item.getProperty("X-GOOGLE-EDITURL") +
        "\n\tcreated:" + item.getProperty("CREATED") +
        "\n\tupdated:" + item.getProperty("LAST-MODIFIED") +
        "\n\ttitle:" + item.title +
        "\n\tcontent:" + item.getProperty("DESCRIPTION") +
        "\n\ttransparency:" + item.getProperty("TRANSP") +
        "\n\tstatus:" + item.status +
        "\n\tstartTime:" + item.startDate.toString() +
        "\n\tendTime:" + item.endDate.toString() +
        "\n\tlocation:" + item.getProperty("LOCATION") +
        "\n\tprivacy:" + item.privacy +
        "\n\tsequence:" + item.getProperty("SEQUENCE") +
        "\n\talarmLastAck:" + item.alarmLastAck +
        "\n\tsnoozeTime:" + item.getProperty("X-MOZ-SNOOZE-TIME") +
        "\n\tisOccurrence: " + (item.recurrenceId != null) +
        "\n\tOrganizer: " + LOGattendee(item.organizer) +
        "\n\tAttendees: " + attendeeString +
        "\n\trecurrence: " + (rstr.length > 1 ? "yes: " + rstr : "no") +
        "\n\talarms: " + (astr.length > 1 ? "yes: " + astr : "no"));
}

function LOGattendee(aAttendee, asString) {
    return aAttendee &&
        ("\n\t\tID: " + aAttendee.id +
         "\n\t\t\tName: " + aAttendee.commonName +
         "\n\t\t\tRsvp: " + aAttendee.rsvp +
         "\n\t\t\tIs Organizer: " +  (aAttendee.isOrganizer ? "yes" : "no") +
         "\n\t\t\tRole: " + aAttendee.role +
         "\n\t\t\tStatus: " + aAttendee.participationStatus);
}

function LOGalarm(aAlarm) {
    if (!aAlarm) {
        return "";
    }

    let enumerator = aAlarm.propertyEnumerator;
    let xpropstr = "";
    while (enumerator.hasMoreElements()) {
        let el = enumerator.getNext();
        xpropstr += "\n\t\t\t" + el.key + ":" + el.value;
    }

    return ("\n\t\tAction: " +  aAlarm.action +
            "\n\t\tOffset: " + (aAlarm.offset && aAlarm.offset.toString()) +
            "\n\t\talarmDate: " + (aAlarm.alarmDate && aAlarm.alarmDate.toString()) +
            "\n\t\trelated: " + aAlarm.related +
            "\n\t\trepeat: " + aAlarm.repeat +
            "\n\t\trepeatOffset: " + (aAlarm.repeatOffset && aAlarm.repeatOffset.toString()) +
            "\n\t\trepeatDate: " + (aAlarm.repeatDate && aAlarm.repeatDate.toString()) +
            "\n\t\tdescription: " + aAlarm.description +
            "\n\t\tsummary: " + aAlarm.summary +
            "\n\t\tproperties: " + (xpropstr.length > 0 ? "yes:" + xpropstr : "no"));
}

function LOGinterval(aInterval) {
    const fbtypes = Components.interfaces.calIFreeBusyInterval;
    if (aInterval.freeBusyType == fbtypes.FREE) {
        type = "FREE";
    } else if (aInterval.freeBusyType == fbtypes.BUSY) {
        type = "BUSY";
    } else {
        type = aInterval.freeBusyType + "(UNKNOWN)";
    }

    cal.LOG("[calGoogleCalendar] Interval from " +
            aInterval.interval.start + " to " + aInterval.interval.end +
            " is " + type);
}