diff options
Diffstat (limited to 'content/includes/calendarsync.js')
-rw-r--r-- | content/includes/calendarsync.js | 421 |
1 files changed, 421 insertions, 0 deletions
diff --git a/content/includes/calendarsync.js b/content/includes/calendarsync.js new file mode 100644 index 0000000..7592060 --- /dev/null +++ b/content/includes/calendarsync.js @@ -0,0 +1,421 @@ +/* + * This file is part of EAS-4-TbSync. + * + * 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/. + */ + +"use strict"; + +var { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetters(this, { + CalAlarm: "resource:///modules/CalAlarm.jsm", + CalAttachment: "resource:///modules/CalAttachment.jsm", + CalAttendee: "resource:///modules/CalAttendee.jsm", + CalEvent: "resource:///modules/CalEvent.jsm", + CalTodo: "resource:///modules/CalTodo.jsm", +}); + +const cal = TbSync.lightning.cal; +const ICAL = TbSync.lightning.ICAL; + +var Calendar = { + + // --------------------------------------------------------------------------- // + // Read WBXML and set Thunderbird item + // --------------------------------------------------------------------------- // + setThunderbirdItemFromWbxml: function (tbItem, data, id, syncdata, mode = "standard") { + + let item = tbItem instanceof TbSync.lightning.TbItem ? tbItem.nativeItem : tbItem; + + let asversion = syncdata.accountData.getAccountProperty("asversion"); + item.id = id; + eas.sync.setItemSubject(item, syncdata, data); + if (TbSync.prefs.getIntPref("log.userdatalevel") > 2) TbSync.dump("Processing " + mode + " calendar item", item.title + " (" + id + ")"); + + eas.sync.setItemLocation(item, syncdata, data); + eas.sync.setItemCategories(item, syncdata, data); + eas.sync.setItemBody(item, syncdata, data); + + //timezone + let stdOffset = eas.defaultTimezoneInfo.std.offset; + let dstOffset = eas.defaultTimezoneInfo.dst.offset; + let easTZ = new eas.tools.TimeZoneDataStructure(); + if (data.TimeZone) { + if (data.TimeZone == "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") { + TbSync.dump("Recieve TZ", "No timezone data received, using local default timezone."); + } else { + //load timezone struct into EAS TimeZone object + easTZ.easTimeZone64 = data.TimeZone; + if (TbSync.prefs.getIntPref("log.userdatalevel") > 2) TbSync.dump("Recieve TZ", item.title + easTZ.toString()); + stdOffset = easTZ.utcOffset; + dstOffset = easTZ.daylightBias + easTZ.utcOffset; + } + } + let timezone = eas.tools.guessTimezoneByStdDstOffset(stdOffset, dstOffset, easTZ.standardName); + + if (data.StartTime) { + let utc = cal.createDateTime(data.StartTime); //format "19800101T000000Z" - UTC + item.startDate = utc.getInTimezone(timezone); + if (data.AllDayEvent && data.AllDayEvent == "1") { + item.startDate.timezone = (cal.dtz && cal.dtz.floating) ? cal.dtz.floating : cal.floating(); + item.startDate.isDate = true; + } + } + + if (data.EndTime) { + let utc = cal.createDateTime(data.EndTime); + item.endDate = utc.getInTimezone(timezone); + if (data.AllDayEvent && data.AllDayEvent == "1") { + item.endDate.timezone = (cal.dtz && cal.dtz.floating) ? cal.dtz.floating : cal.floating(); + item.endDate.isDate = true; + } + } + + //stamp time cannot be set and it is not needed, an updated version is only send to the server, if there was a change, so stamp will be updated + + + //EAS Reminder + item.clearAlarms(); + if (data.Reminder && data.StartTime) { + let alarm = new CalAlarm(); + alarm.related = Components.interfaces.calIAlarm.ALARM_RELATED_START; + alarm.offset = cal.createDuration(); + alarm.offset.inSeconds = (0-parseInt(data.Reminder)*60); + alarm.action ="DISPLAY"; + item.addAlarm(alarm); + + let alarmData = cal.alarms.calculateAlarmDate(item, alarm); + let startDate = cal.createDateTime(data.StartTime); + let nowDate = eas.tools.getNowUTC(); + if (startDate.compare(nowDate) < 0) { + // Mark alarm as ACK if in the past. + item.alarmLastAck = nowDate; + } + } + + eas.sync.mapEasPropertyToThunderbird ("BusyStatus", "TRANSP", data, item); + eas.sync.mapEasPropertyToThunderbird ("Sensitivity", "CLASS", data, item); + + if (data.ResponseType) { + //store original EAS value + item.setProperty("X-EAS-ResponseType", eas.xmltools.checkString(data.ResponseType, "0")); //some server send empty ResponseType ??? + } + + //Attendees - remove all Attendees and re-add the ones from XML + item.removeAllAttendees(); + if (data.Attendees && data.Attendees.Attendee) { + let att = []; + if (Array.isArray(data.Attendees.Attendee)) att = data.Attendees.Attendee; + else att.push(data.Attendees.Attendee); + for (let i = 0; i < att.length; i++) { + if (att[i].Email && eas.tools.isString(att[i].Email) && att[i].Name) { //req. + + let attendee = new CalAttendee(); + + //is this attendee the local EAS user? + let isSelf = (att[i].Email == syncdata.accountData.getAccountProperty("user")); + + attendee["id"] = cal.email.prependMailTo(att[i].Email); + attendee["commonName"] = att[i].Name; + //default is "FALSE", only if THIS attendee isSelf, use ResponseRequested (we cannot respond for other attendee) - ResponseType is not send back to the server, it is just a local information + attendee["rsvp"] = (isSelf && data.ResponseRequested) ? "TRUE" : "FALSE"; + + //not supported in 2.5 + switch (att[i].AttendeeType) { + case "1": //required + attendee["role"] = "REQ-PARTICIPANT"; + attendee["userType"] = "INDIVIDUAL"; + break; + case "2": //optional + attendee["role"] = "OPT-PARTICIPANT"; + attendee["userType"] = "INDIVIDUAL"; + break; + default : //resource or unknown + attendee["role"] = "NON-PARTICIPANT"; + attendee["userType"] = "RESOURCE"; + break; + } + + //not supported in 2.5 - if attendeeStatus is missing, check if this isSelf and there is a ResponseType + if (att[i].AttendeeStatus) + attendee["participationStatus"] = eas.sync.MAP_EAS2TB.ATTENDEESTATUS[att[i].AttendeeStatus]; + else if (isSelf && data.ResponseType) + attendee["participationStatus"] = eas.sync.MAP_EAS2TB.ATTENDEESTATUS[data.ResponseType]; + else + attendee["participationStatus"] = "NEEDS-ACTION"; + + // status : [NEEDS-ACTION, ACCEPTED, DECLINED, TENTATIVE, DELEGATED, COMPLETED, IN-PROCESS] + // rolemap : [REQ-PARTICIPANT, OPT-PARTICIPANT, NON-PARTICIPANT, CHAIR] + // typemap : [INDIVIDUAL, GROUP, RESOURCE, ROOM] + + // Add attendee to event + item.addAttendee(attendee); + } else { + TbSync.eventlog.add("info", syncdata, "Attendee without required name and/or email found. Skipped."); + } + } + } + + if (data.OrganizerName && data.OrganizerEmail && eas.tools.isString(data.OrganizerEmail)) { + //Organizer + let organizer = new CalAttendee(); + organizer.id = cal.email.prependMailTo(data.OrganizerEmail); + organizer.commonName = data.OrganizerName; + organizer.rsvp = "FALSE"; + organizer.role = "CHAIR"; + organizer.userType = null; + organizer.participationStatus = "ACCEPTED"; + organizer.isOrganizer = true; + item.organizer = organizer; + } + + eas.sync.setItemRecurrence(item, syncdata, data, timezone); + + // BusyStatus is always representing the status of the current user in terms of availability. + // It has nothing to do with the status of a meeting. The user could be just the organizer, but does not need to attend, so he would be free. + // The correct map is between BusyStatus and TRANSP (show time as avail, busy, unset) + // A new event always sets TRANSP to busy, so unset is indeed a good way to store Tentiative + // However: + // - EAS Meetingstatus only knows ACTIVE or CANCELLED, but not CONFIRMED or TENTATIVE + // - TB STATUS has UNSET, CONFIRMED, TENTATIVE, CANCELLED + // -> Special case: User sets BusyStatus to TENTIATIVE -> TRANSP is unset and also set STATUS to TENTATIVE + // The TB STATUS is the correct map for EAS Meetingstatus and should be unset, if it is not a meeting EXCEPT if set to TENTATIVE + let tbStatus = (data.BusyStatus && data.BusyStatus == "1" ? "TENTATIVE" : null); + + if (data.MeetingStatus) { + //store original EAS value + item.setProperty("X-EAS-MeetingStatus", data.MeetingStatus); + //bitwise representation for Meeting, Received, Cancelled: + let M = data.MeetingStatus & 0x1; + let R = data.MeetingStatus & 0x2; + let C = data.MeetingStatus & 0x4; + + // We can map M+C to TB STATUS (TENTATIVE, CONFIRMED, CANCELLED, unset). + if (M) { + if (C) tbStatus = "CANCELLED"; + else if (!tbStatus) tbStatus = "CONFIRMED"; // do not override "TENTIATIVE" + } + + //we can also use the R information, to update our fallbackOrganizerName + if (!R && data.OrganizerName) syncdata.target.calendar.setProperty("fallbackOrganizerName", data.OrganizerName); + } + + if (tbStatus) item.setProperty("STATUS", tbStatus) + else item.deleteProperty("STATUS"); + + //TODO: attachements (needs EAS 16.0!) + }, + + + + + + + + + + // --------------------------------------------------------------------------- // + //read TB event and return its data as WBXML + // --------------------------------------------------------------------------- // + getWbxmlFromThunderbirdItem: async function (tbItem, syncdata, isException = false) { + let item = tbItem instanceof TbSync.lightning.TbItem ? tbItem.nativeItem : tbItem; + + let asversion = syncdata.accountData.getAccountProperty("asversion"); + let wbxml = eas.wbxmltools.createWBXML("", syncdata.type); //init wbxml with "" and not with precodes, and set initial codepage + let nowDate = new Date(); + + /* + * We do not use ghosting, that means, if we do not include a value in CHANGE, it is removed from the server. + * However, this does not seem to work on all fields. Furthermore, we need to include any (empty) container to blank its childs. + */ + + //Order of tags taken from https://msdn.microsoft.com/en-us/library/dn338917(v=exchg.80).aspx + + //timezone + if (!isException) { + let easTZ = new eas.tools.TimeZoneDataStructure(); + + //if there is no end and no start (or both are floating) use default timezone info + let tzInfo = null; + if (item.startDate && item.startDate.timezone.tzid != "floating") tzInfo = eas.tools.getTimezoneInfo(item.startDate.timezone); + else if (item.endDate && item.endDate.timezone.tzid != "floating") tzInfo = eas.tools.getTimezoneInfo(item.endDate.timezone); + if (!tzInfo) tzInfo = eas.defaultTimezoneInfo; + + easTZ.utcOffset = tzInfo.std.offset; + easTZ.standardBias = 0; + easTZ.daylightBias = tzInfo.dst.offset - tzInfo.std.offset; + + easTZ.standardName = eas.ianaToWindowsTimezoneMap.hasOwnProperty(tzInfo.std.displayname) ? eas.ianaToWindowsTimezoneMap[tzInfo.std.displayname] : tzInfo.std.displayname; + easTZ.daylightName = eas.ianaToWindowsTimezoneMap.hasOwnProperty(tzInfo.dst.displayname) ? eas.ianaToWindowsTimezoneMap[tzInfo.dst.displayname] : tzInfo.dst.displayname; + + if (tzInfo.std.switchdate && tzInfo.dst.switchdate) { + easTZ.standardDate.wMonth = tzInfo.std.switchdate.month; + easTZ.standardDate.wDay = tzInfo.std.switchdate.weekOfMonth; + easTZ.standardDate.wDayOfWeek = tzInfo.std.switchdate.dayOfWeek; + easTZ.standardDate.wHour = tzInfo.std.switchdate.hour; + easTZ.standardDate.wMinute = tzInfo.std.switchdate.minute; + easTZ.standardDate.wSecond = tzInfo.std.switchdate.second; + + easTZ.daylightDate.wMonth = tzInfo.dst.switchdate.month; + easTZ.daylightDate.wDay = tzInfo.dst.switchdate.weekOfMonth; + easTZ.daylightDate.wDayOfWeek = tzInfo.dst.switchdate.dayOfWeek; + easTZ.daylightDate.wHour = tzInfo.dst.switchdate.hour; + easTZ.daylightDate.wMinute = tzInfo.dst.switchdate.minute; + easTZ.daylightDate.wSecond = tzInfo.dst.switchdate.second; + } + + wbxml.atag("TimeZone", easTZ.easTimeZone64); + if (TbSync.prefs.getIntPref("log.userdatalevel") > 2) TbSync.dump("Send TZ", item.title + easTZ.toString()); + } + + //AllDayEvent (for simplicity, we always send a value) + wbxml.atag("AllDayEvent", (item.startDate && item.startDate.isDate && item.endDate && item.endDate.isDate) ? "1" : "0"); + + //Body + wbxml.append(eas.sync.getItemBody(item, syncdata)); + + //BusyStatus (Free, Tentative, Busy) is taken from TRANSP (busy, free, unset=tentative) + //However if STATUS is set to TENTATIVE, overide TRANSP and set BusyStatus to TENTATIVE + if (item.hasProperty("STATUS") && item.getProperty("STATUS") == "TENTATIVE") { + wbxml.atag("BusyStatus","1"); + } else { + wbxml.atag("BusyStatus", eas.sync.mapThunderbirdPropertyToEas("TRANSP", "BusyStatus", item)); + } + + //Organizer + if (!isException) { + if (item.organizer && item.organizer.commonName) wbxml.atag("OrganizerName", item.organizer.commonName); + if (item.organizer && item.organizer.id) wbxml.atag("OrganizerEmail", cal.email.removeMailTo(item.organizer.id)); + } + + //DtStamp in UTC + wbxml.atag("DtStamp", item.stampTime ? eas.tools.getIsoUtcString(item.stampTime) : eas.tools.dateToBasicISOString(nowDate)); + + //EndTime in UTC + wbxml.atag("EndTime", item.endDate ? eas.tools.getIsoUtcString(item.endDate) : eas.tools.dateToBasicISOString(nowDate)); + + //Location + wbxml.atag("Location", (item.hasProperty("location")) ? item.getProperty("location") : ""); + + //EAS Reminder (TB getAlarms) - at least with zpush blanking by omitting works, horde does not work + let alarms = item.getAlarms({}); + if (alarms.length>0) { + + let reminder = -1; + if (alarms[0].offset !== null) { + reminder = 0 - alarms[0].offset.inSeconds/60; + } else if (item.startDate) { + let timeDiff =item.startDate.getInTimezone(eas.utcTimezone).subtractDate(alarms[0].alarmDate.getInTimezone(eas.utcTimezone)); + reminder = timeDiff.inSeconds/60; + TbSync.eventlog.add("info", syncdata, "Converting absolute alarm to relative alarm (not supported).", item.icalString); + } + if (reminder >= 0) wbxml.atag("Reminder", reminder.toString()); + else TbSync.eventlog.add("info", syncdata, "Droping alarm after start date (not supported).", item.icalString); + + } + + //Sensitivity (CLASS) + wbxml.atag("Sensitivity", eas.sync.mapThunderbirdPropertyToEas("CLASS", "Sensitivity", item)); + + //Subject (obmitting these, should remove them from the server - that does not work reliably, so we send blanks) + wbxml.atag("Subject", (item.title) ? item.title : ""); + + //StartTime in UTC + wbxml.atag("StartTime", item.startDate ? eas.tools.getIsoUtcString(item.startDate) : eas.tools.dateToBasicISOString(nowDate)); + + //UID (limit to 300) + //each TB event has an ID, which is used as EAS serverId - however there is a second UID in the ApplicationData + //since we do not have two different IDs to use, we use the same ID + if (!isException) { //docs say it would be allowed in exception in 2.5, but it does not work, if present + wbxml.atag("UID", item.id); + } + //IMPORTANT in EAS v16 it is no longer allowed to send a UID + //Only allowed in exceptions in v2.5 + + + //EAS MeetingStatus + // 0 (000) The event is an appointment, which has no attendees. + // 1 (001) The event is a meeting and the user is the meeting organizer. + // 3 (011) This event is a meeting, and the user is not the meeting organizer; the meeting was received from someone else. + // 5 (101) The meeting has been canceled and the user was the meeting organizer. + // 7 (111) The meeting has been canceled. The user was not the meeting organizer; the meeting was received from someone else + + //there are 3 fields; Meeting, Owner, Cancelled + //M can be reconstructed from #of attendees (looking at the old value is not wise, since it could have been changed) + //C can be reconstucted from TB STATUS + //O can be reconstructed by looking at the original value, or (if not present) by comparing EAS ownerID with TB ownerID + + let attendees = item.getAttendees(); + //if (!(isException && asversion == "2.5")) { //MeetingStatus is not supported in exceptions in EAS 2.5 + if (!isException) { //Exchange 2010 does not seem to support MeetingStatus at all in exceptions + if (attendees.length == 0) wbxml.atag("MeetingStatus", "0"); + else { + //get owner information + let isReceived = false; + if (item.hasProperty("X-EAS-MEETINGSTATUS")) isReceived = item.getProperty("X-EAS-MEETINGSTATUS") & 0x2; + else isReceived = (item.organizer && item.organizer.id && cal.email.removeMailTo(item.organizer.id) != syncdata.accountData.getAccountProperty("user")); + + //either 1,3,5 or 7 + if (item.hasProperty("STATUS") && item.getProperty("STATUS") == "CANCELLED") { + //either 5 or 7 + wbxml.atag("MeetingStatus", (isReceived ? "7" : "5")); + } else { + //either 1 or 3 + wbxml.atag("MeetingStatus", (isReceived ? "3" : "1")); + } + } + } + + //Attendees + let TB_responseType = null; + if (!(isException && asversion == "2.5")) { //attendees are not supported in exceptions in EAS 2.5 + if (attendees.length > 0) { //We should use it instead of countAttendees.value + wbxml.otag("Attendees"); + for (let attendee of attendees) { + wbxml.otag("Attendee"); + wbxml.atag("Email", cal.email.removeMailTo(attendee.id)); + wbxml.atag("Name", (attendee.commonName ? attendee.commonName : cal.email.removeMailTo(attendee.id).split("@")[0])); + if (asversion != "2.5") { + //it's pointless to send AttendeeStatus, + // - if we are the owner of a meeting, TB does not have an option to actually set the attendee status (on behalf of an attendee) in the UI + // - if we are an attendee (of an invite) we cannot and should not set status of other attendees and or own status must be send through a MeetingResponse + // -> all changes of attendee status are send from the server to us, either via ResponseType or via AttendeeStatus + //wbxml.atag("AttendeeStatus", eas.sync.MAP_TB2EAS.ATTENDEESTATUS[attendee.participationStatus]); + + if (attendee.userType == "RESOURCE" || attendee.userType == "ROOM" || attendee.role == "NON-PARTICIPANT") wbxml.atag("AttendeeType","3"); + else if (attendee.role == "REQ-PARTICIPANT" || attendee.role == "CHAIR") wbxml.atag("AttendeeType","1"); + else wbxml.atag("AttendeeType","2"); //leftovers are optional + } + wbxml.ctag(); + } + wbxml.ctag(); + } else { + wbxml.atag("Attendees"); + } + } + + //Categories (see https://github.com/jobisoft/TbSync/pull/35#issuecomment-359286374) + if (!isException) { + wbxml.append(eas.sync.getItemCategories(item, syncdata)); + } + + //recurrent events (implemented by Chris Allan) + if (!isException) { + wbxml.append(await eas.sync.getItemRecurrence(item, syncdata)); + } + + + //--------------------------- + + //TP PRIORITY (9=LOW, 5=NORMAL, 1=HIGH) not mapable to EAS Event + //TODO: attachements (needs EAS 16.0!) + + //https://dxr.mozilla.org/comm-central/source/calendar/base/public/calIAlarm.idl + //TbSync.dump("ALARM ("+i+")", [, alarms[i].related, alarms[i].repeat, alarms[i].repeatOffset, alarms[i].repeatDate, alarms[i].action].join("|")); + + return wbxml.getBytes(); + } +} |