/* * 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(); } }