diff options
Diffstat (limited to 'content/includes/tools.js')
-rw-r--r-- | content/includes/tools.js | 529 |
1 files changed, 529 insertions, 0 deletions
diff --git a/content/includes/tools.js b/content/includes/tools.js new file mode 100644 index 0000000..a2298b9 --- /dev/null +++ b/content/includes/tools.js @@ -0,0 +1,529 @@ +/* + * 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 tools = { + + setCalItemProperty: function (item, prop, value) { + if (value == "unset") item.deleteProperty(prop); + else item.setProperty(prop, value); + }, + + getCalItemProperty: function (item, prop) { + if (item.hasProperty(prop)) return item.getProperty(prop); + else return "unset"; + }, + + isString: function (s) { + return (typeof s == 'string' || s instanceof String); + }, + + getIdentityKey: function (email) { + for (let account of MailServices.accounts.accounts) { + if (account.defaultIdentity && account.defaultIdentity.email == email) return account.defaultIdentity.key; + } + return ""; + }, + + parentIsTrash: function (folderData) { + let parentID = folderData.getFolderProperty("parentID"); + if (parentID == "0") return false; + + let parentFolder = folderData.accountData.getFolder("serverID", parentID); + if (parentFolder && parentFolder.getFolderProperty("type") == "4") return true; + + return false; + }, + + getNewDeviceId: function () { + //taken from https://jsfiddle.net/briguy37/2MVFd/ + let d = new Date().getTime(); + let uuid = 'xxxxxxxxxxxxxxxxyxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + let r = (d + Math.random()*16)%16 | 0; + d = Math.floor(d/16); + return (c=='x' ? r : (r&0x3|0x8)).toString(16); + }); + return "MZTB" + uuid; + }, + + getUriFromDirectoryId: function(ownerId) { + let directories = MailServices.ab.directories; + for (let directory of directories) { + if (directory instanceof Components.interfaces.nsIAbDirectory) { + if (ownerId.startsWith(directory.dirPrefId)) return directory.URI; + } + } + return null; + }, + + //function to get correct uri of current card for global book as well for mailLists + getSelectedUri : function(aUri, aCard) { + if (aUri == "moz-abdirectory://?") { + //get parent via card owner + return eas.tools.getUriFromDirectoryId(aCard.directoryId); + } else if (MailServices.ab.getDirectory(aUri).isMailList) { + //MailList suck, we have to cut the url to get the parent + return aUri.substring(0, aUri.lastIndexOf("/")) + } else { + return aUri; + } + }, + + //read file from within the XPI package + fetchFile: function (aURL, returnType = "Array") { + return new Promise((resolve, reject) => { + let uri = Services.io.newURI(aURL); + let channel = Services.io.newChannelFromURI(uri, + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Components.interfaces.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT, + Components.interfaces.nsIContentPolicy.TYPE_OTHER); + + NetUtil.asyncFetch(channel, (inputStream, status) => { + if (!Components.isSuccessCode(status)) { + reject(status); + return; + } + + try { + let data = NetUtil.readInputStreamToString(inputStream, inputStream.available()); + if (returnType == "Array") { + resolve(data.replace("\r","").split("\n")) + } else { + resolve(data); + } + } catch (ex) { + reject(ex); + } + }); + }); + }, + + + + + + + + + + + // TIMEZONE STUFF + + TimeZoneDataStructure : class { + constructor() { + this.buf = new DataView(new ArrayBuffer(172)); + } + +/* + Buffer structure: + @000 utcOffset (4x8bit as 1xLONG) + + @004 standardName (64x8bit as 32xWCHAR) + @068 standardDate (16x8 as 1xSYSTEMTIME) + @084 standardBias (4x8bit as 1xLONG) + + @088 daylightName (64x8bit as 32xWCHAR) + @152 daylightDate (16x8 as 1xSTRUCT) + @168 daylightBias (4x8bit as 1xLONG) +*/ + + set easTimeZone64 (b64) { + //clear buffer + for (let i=0; i<172; i++) this.buf.setUint8(i, 0); + //load content into buffer + let content = (b64 == "") ? "" : atob(b64); + for (let i=0; i<content.length; i++) this.buf.setUint8(i, content.charCodeAt(i)); + } + + get easTimeZone64 () { + let content = ""; + for (let i=0; i<172; i++) content += String.fromCharCode(this.buf.getUint8(i)); + return (btoa(content)); + } + + getstr (byteoffset) { + let str = ""; + //walk thru the buffer in 32 steps of 16bit (wchars) + for (let i=0;i<32;i++) { + let cc = this.buf.getUint16(byteoffset+i*2, true); + if (cc == 0) break; + str += String.fromCharCode(cc); + } + return str; + } + + setstr (byteoffset, str) { + //clear first + for (let i=0;i<32;i++) this.buf.setUint16(byteoffset+i*2, 0); + //walk thru the buffer in steps of 16bit (wchars) + for (let i=0;i<str.length && i<32; i++) this.buf.setUint16(byteoffset+i*2, str.charCodeAt(i), true); + } + + getsystemtime (buf, offset) { + let systemtime = { + get wYear () { return buf.getUint16(offset + 0, true); }, + get wMonth () { return buf.getUint16(offset + 2, true); }, + get wDayOfWeek () { return buf.getUint16(offset + 4, true); }, + get wDay () { return buf.getUint16(offset + 6, true); }, + get wHour () { return buf.getUint16(offset + 8, true); }, + get wMinute () { return buf.getUint16(offset + 10, true); }, + get wSecond () { return buf.getUint16(offset + 12, true); }, + get wMilliseconds () { return buf.getUint16(offset + 14, true); }, + toString() { return [this.wYear, this.wMonth, this.wDay].join("-") + ", " + this.wDayOfWeek + ", " + [this.wHour,this.wMinute,this.wSecond].join(":") + "." + this.wMilliseconds}, + + set wYear (v) { buf.setUint16(offset + 0, v, true); }, + set wMonth (v) { buf.setUint16(offset + 2, v, true); }, + set wDayOfWeek (v) { buf.setUint16(offset + 4, v, true); }, + set wDay (v) { buf.setUint16(offset + 6, v, true); }, + set wHour (v) { buf.setUint16(offset + 8, v, true); }, + set wMinute (v) { buf.setUint16(offset + 10, v, true); }, + set wSecond (v) { buf.setUint16(offset + 12, v, true); }, + set wMilliseconds (v) { buf.setUint16(offset + 14, v, true); }, + }; + return systemtime; + } + + get standardDate () {return this.getsystemtime (this.buf, 68); } + get daylightDate () {return this.getsystemtime (this.buf, 152); } + + get utcOffset () { return this.buf.getInt32(0, true); } + set utcOffset (v) { this.buf.setInt32(0, v, true); } + + get standardBias () { return this.buf.getInt32(84, true); } + set standardBias (v) { this.buf.setInt32(84, v, true); } + get daylightBias () { return this.buf.getInt32(168, true); } + set daylightBias (v) { this.buf.setInt32(168, v, true); } + + get standardName () {return this.getstr(4); } + set standardName (v) {return this.setstr(4, v); } + get daylightName () {return this.getstr(88); } + set daylightName (v) {return this.setstr(88, v); } + + toString () { return ["", + "utcOffset: "+ this.utcOffset, + "standardName: "+ this.standardName, + "standardDate: "+ this.standardDate.toString(), + "standardBias: "+ this.standardBias, + "daylightName: "+ this.daylightName, + "daylightDate: "+ this.daylightDate.toString(), + "daylightBias: "+ this.daylightBias].join("\n"); } + }, + + + //Date has a toISOString method, which returns the Date obj as extended ISO 8601, + //however EAS MS-ASCAL uses compact/basic ISO 8601, + dateToBasicISOString : function (date) { + function pad(number) { + if (number < 10) { + return '0' + number; + } + return number.toString(); + } + + return pad(date.getUTCFullYear()) + + pad(date.getUTCMonth() + 1) + + pad(date.getUTCDate()) + + 'T' + + pad(date.getUTCHours()) + + pad(date.getUTCMinutes()) + + pad(date.getUTCSeconds()) + + 'Z'; + }, + + + //Save replacement for cal.createDateTime, which accepts compact/basic and also extended ISO 8601, + //cal.createDateTime only supports compact/basic + createDateTime: function(str) { + let datestring = str; + if (str.indexOf("-") == 4) { + //this looks like extended ISO 8601 + let tempDate = new Date(str); + datestring = eas.tools.dateToBasicISOString(tempDate); + } + return TbSync.lightning.cal.createDateTime(datestring); + }, + + + // Convert TB date to UTC and return it as basic or extended ISO 8601 String + getIsoUtcString: function(origdate, requireExtendedISO = false, fakeUTC = false) { + let date = origdate.clone(); + //floating timezone cannot be converted to UTC (cause they float) - we have to overwrite it with the local timezone + if (date.timezone.tzid == "floating") date.timezone = eas.defaultTimezoneInfo.timezone; + //to get the UTC string we could use icalString (which does not work on allDayEvents, or calculate it from nativeTime) + date.isDate = 0; + let UTC = date.getInTimezone(eas.utcTimezone); + if (fakeUTC) UTC = date.clone(); + + function pad(number) { + if (number < 10) { + return '0' + number; + } + return number; + } + + if (requireExtendedISO) { + return UTC.year + + "-" + pad(UTC.month + 1 ) + + "-" + pad(UTC.day) + + "T" + pad(UTC.hour) + + ":" + pad(UTC.minute) + + ":" + pad(UTC.second) + + "." + "000" + + "Z"; + } else { + return UTC.icalString; + } + }, + + getNowUTC : function() { + return TbSync.lightning.cal.dtz.jsDateToDateTime(new Date()).getInTimezone(TbSync.lightning.cal.dtz.UTC); + }, + + //guess the IANA timezone (used by TB) based on the current offset (standard or daylight) + guessTimezoneByCurrentOffset: function(curOffset, utcDateTime) { + //if we only now the current offset and the current date, we need to actually try each TZ. + let tzService = TbSync.lightning.cal.timezoneService; + + //first try default tz + let test = utcDateTime.getInTimezone(eas.defaultTimezoneInfo.timezone); + TbSync.dump("Matching TZ via current offset: " + test.timezone.tzid + " @ " + curOffset, test.timezoneOffset/-60); + if (test.timezoneOffset/-60 == curOffset) return test.timezone; + + //second try UTC + test = utcDateTime.getInTimezone(eas.utcTimezone); + TbSync.dump("Matching TZ via current offset: " + test.timezone.tzid + " @ " + curOffset, test.timezoneOffset/-60); + if (test.timezoneOffset/-60 == curOffset) return test.timezone; + + //third try all others + for (let timezoneId of tzService.timezoneIds) { + let test = utcDateTime.getInTimezone(tzService.getTimezone(timezoneId)); + TbSync.dump("Matching TZ via current offset: " + test.timezone.tzid + " @ " + curOffset, test.timezoneOffset/-60); + if (test.timezoneOffset/-60 == curOffset) return test.timezone; + } + + //return default TZ as fallback + return eas.defaultTimezoneInfo.timezone; + }, + + + //guess the IANA timezone (used by TB) based on stdandard offset, daylight offset and standard name + guessTimezoneByStdDstOffset: function(stdOffset, dstOffset, stdName = "") { + + //get a list of all zones + //alternativly use cal.fromRFC3339 - but this is only doing this: + //https://dxr.mozilla.org/comm-central/source/calendar/base/modules/calProviderUtils.jsm + + //cache timezone data on first attempt + if (eas.cachedTimezoneData === null) { + eas.cachedTimezoneData = {}; + eas.cachedTimezoneData.iana = {}; + eas.cachedTimezoneData.abbreviations = {}; + eas.cachedTimezoneData.stdOffset = {}; + eas.cachedTimezoneData.bothOffsets = {}; + + let tzService = TbSync.lightning.cal.timezoneService; + + //cache timezones data from internal IANA data + for (let timezoneId of tzService.timezoneIds) { + let timezone = tzService.getTimezone(timezoneId); + let tzInfo = eas.tools.getTimezoneInfo(timezone); + + eas.cachedTimezoneData.bothOffsets[tzInfo.std.offset+":"+tzInfo.dst.offset] = timezone; + eas.cachedTimezoneData.stdOffset[tzInfo.std.offset] = timezone; + + eas.cachedTimezoneData.abbreviations[tzInfo.std.abbreviation] = timezoneId; + eas.cachedTimezoneData.iana[timezoneId] = tzInfo; + + //TbSync.dump("TZ ("+ tzInfo.std.id + " :: " + tzInfo.dst.id + " :: " + tzInfo.std.displayname + " :: " + tzInfo.dst.displayname + " :: " + tzInfo.std.offset + " :: " + tzInfo.dst.offset + ")", tzService.getTimezone(id)); + } + + //make sure, that UTC timezone is there + eas.cachedTimezoneData.bothOffsets["0:0"] = eas.utcTimezone; + + //multiple TZ share the same offset and abbreviation, make sure the default timezone is present + eas.cachedTimezoneData.abbreviations[eas.defaultTimezoneInfo.std.abbreviation] = eas.defaultTimezoneInfo.std.id; + eas.cachedTimezoneData.bothOffsets[eas.defaultTimezoneInfo.std.offset+":"+eas.defaultTimezoneInfo.dst.offset] = eas.defaultTimezoneInfo.timezone; + eas.cachedTimezoneData.stdOffset[eas.defaultTimezoneInfo.std.offset] = eas.defaultTimezoneInfo.timezone; + + } + + /* + 1. Try to find name in Windows names and map to IANA -> if found, does the stdOffset match? -> if so, done + 2. Try to parse our own format, split name and test each chunk for IANA -> if found, does the stdOffset match? -> if so, done + 3. Try if one of the chunks matches international code -> if found, does the stdOffset match? -> if so, done + 4. Fallback: Use just the offsets */ + + + //check for windows timezone name + if (eas.windowsToIanaTimezoneMap[stdName] && eas.cachedTimezoneData.iana[eas.windowsToIanaTimezoneMap[stdName]] && eas.cachedTimezoneData.iana[eas.windowsToIanaTimezoneMap[stdName]].std.offset == stdOffset ) { + //the windows timezone maps multiple IANA zones to one (Berlin*, Rome, Bruessel) + //check the windowsZoneName of the default TZ and of the winning, if they match, use default TZ + //so Rome could win, even Berlin is the default IANA zone + if (eas.defaultTimezoneInfo.std.windowsZoneName && eas.windowsToIanaTimezoneMap[stdName] != eas.defaultTimezoneInfo.std.id && eas.cachedTimezoneData.iana[eas.windowsToIanaTimezoneMap[stdName]].std.offset == eas.defaultTimezoneInfo.std.offset && stdName == eas.defaultTimezoneInfo.std.windowsZoneName) { + TbSync.dump("Timezone matched via windows timezone name ("+stdName+") with default TZ overtake", eas.windowsToIanaTimezoneMap[stdName] + " -> " + eas.defaultTimezoneInfo.std.id); + return eas.defaultTimezoneInfo.timezone; + } + + TbSync.dump("Timezone matched via windows timezone name ("+stdName+")", eas.windowsToIanaTimezoneMap[stdName]); + return eas.cachedTimezoneData.iana[eas.windowsToIanaTimezoneMap[stdName]].timezone; + } + + let parts = stdName.replace(/[;,()\[\]]/g," ").split(" "); + for (let i = 0; i < parts.length; i++) { + //check for IANA + if (eas.cachedTimezoneData.iana[parts[i]] && eas.cachedTimezoneData.iana[parts[i]].std.offset == stdOffset) { + TbSync.dump("Timezone matched via IANA", parts[i]); + return eas.cachedTimezoneData.iana[parts[i]].timezone; + } + + //check for international abbreviation for standard period (CET, CAT, ...) + if (eas.cachedTimezoneData.abbreviations[parts[i]] && eas.cachedTimezoneData.iana[eas.cachedTimezoneData.abbreviations[parts[i]]] && eas.cachedTimezoneData.iana[eas.cachedTimezoneData.abbreviations[parts[i]]].std.offset == stdOffset) { + TbSync.dump("Timezone matched via international abbreviation (" + parts[i] +")", eas.cachedTimezoneData.abbreviations[parts[i]]); + return eas.cachedTimezoneData.iana[eas.cachedTimezoneData.abbreviations[parts[i]]].timezone; + } + } + + //fallback to zone based on stdOffset and dstOffset, if we have that cached + if (eas.cachedTimezoneData.bothOffsets[stdOffset+":"+dstOffset]) { + TbSync.dump("Timezone matched via both offsets (std:" + stdOffset +", dst:" + dstOffset + ")", eas.cachedTimezoneData.bothOffsets[stdOffset+":"+dstOffset].tzid); + return eas.cachedTimezoneData.bothOffsets[stdOffset+":"+dstOffset]; + } + + //fallback to zone based on stdOffset only, if we have that cached + if (eas.cachedTimezoneData.stdOffset[stdOffset]) { + TbSync.dump("Timezone matched via std offset (" + stdOffset +")", eas.cachedTimezoneData.stdOffset[stdOffset].tzid); + return eas.cachedTimezoneData.stdOffset[stdOffset]; + } + + //return default timezone, if everything else fails + TbSync.dump("Timezone could not be matched via offsets (std:" + stdOffset +", dst:" + dstOffset + "), using default timezone", eas.defaultTimezoneInfo.std.id); + return eas.defaultTimezoneInfo.timezone; + }, + + + //extract standard and daylight timezone data + getTimezoneInfo: function (timezone) { + let tzInfo = {}; + + tzInfo.std = eas.tools.getTimezoneInfoObject(timezone, "standard"); + tzInfo.dst = eas.tools.getTimezoneInfoObject(timezone, "daylight"); + + if (tzInfo.dst === null) tzInfo.dst = tzInfo.std; + + tzInfo.timezone = timezone; + return tzInfo; + }, + + + //get timezone info for standard/daylight + getTimezoneInfoObject: function (timezone, standardOrDaylight) { + + //handle UTC + if (timezone.isUTC) { + let obj = {} + obj.id = "UTC"; + obj.offset = 0; + obj.abbreviation = "UTC"; + obj.displayname = "Coordinated Universal Time (UTC)"; + return obj; + } + + //we could parse the icalstring by ourself, but I wanted to use ICAL.parse - TODO try catch + let info = TbSync.lightning.ICAL.parse("BEGIN:VCALENDAR\r\n" + timezone.icalComponent.toString() + "\r\nEND:VCALENDAR"); + let comp = new TbSync.lightning.ICAL.Component(info); + let vtimezone =comp.getFirstSubcomponent("vtimezone"); + let id = vtimezone.getFirstPropertyValue("tzid").toString(); + let zone = vtimezone.getFirstSubcomponent(standardOrDaylight); + + if (zone) { + let obj = {}; + obj.id = id; + + //get offset + let utcOffset = zone.getFirstPropertyValue("tzoffsetto").toString(); + let o = parseInt(utcOffset.replace(":","")); //-330 = - 3h 30min + let h = Math.floor(o / 100); //-3 -> -180min + let m = o - (h*100) //-330 - -300 = -30 + obj.offset = -1*((h*60) + m); + + //get international abbreviation (CEST, CET, CAT ... ) + obj.abbreviation = ""; + try { + obj.abbreviation = zone.getFirstPropertyValue("tzname").toString(); + } catch(e) { + TbSync.dump("Failed TZ", timezone.icalComponent.toString()); + } + + //get displayname + obj.displayname = /*"("+utcOffset+") " +*/ obj.id;// + ", " + obj.abbreviation; + + //get DST switch date + let rrule = zone.getFirstPropertyValue("rrule"); + let dtstart = zone.getFirstPropertyValue("dtstart"); + if (rrule && dtstart) { + /* + + THE switchdate PART OF THE OBJECT IS MICROSOFT SPECIFIC, EVERYTHING ELSE IS THUNDERBIRD GENERIC, I LET IT SIT HERE ANYHOW + + https://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx + + To select the correct day in the month, set the wYear member to zero, the wHour and wMinute members to + the transition time, the wDayOfWeek member to the appropriate weekday, and the wDay member to indicate + the occurrence of the day of the week within the month (1 to 5, where 5 indicates the final occurrence during the + month if that day of the week does not occur 5 times). + + Using this notation, specify 02:00 on the first Sunday in April as follows: + wHour = 2, wMonth = 4, wDayOfWeek = 0, wDay = 1. + Specify 02:00 on the last Thursday in October as follows: + wHour = 2, wMonth = 10, wDayOfWeek = 4, wDay = 5. + + So we have to parse the RRULE to exract wDay + RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 */ + + let parts =rrule.toString().split(";"); + let rules = {}; + for (let i = 0; i< parts.length; i++) { + let sub = parts[i].split("="); + if (sub.length == 2) rules[sub[0]] = sub[1]; + } + + if (rules.FREQ == "YEARLY" && rules.BYDAY && rules.BYMONTH && rules.BYDAY.length > 2) { + obj.switchdate = {}; + obj.switchdate.month = parseInt(rules.BYMONTH); + + let days = ["SU","MO","TU","WE","TH","FR","SA"]; + obj.switchdate.dayOfWeek = days.indexOf(rules.BYDAY.substring(rules.BYDAY.length-2)); + obj.switchdate.weekOfMonth = parseInt(rules.BYDAY.substring(0, rules.BYDAY.length-2)); + if (obj.switchdate.weekOfMonth<0 || obj.switchdate.weekOfMonth>5) obj.switchdate.weekOfMonth = 5; + + //get switch time from dtstart + let dttime = eas.tools.createDateTime(dtstart.toString()); + obj.switchdate.hour = dttime.hour; + obj.switchdate.minute = dttime.minute; + obj.switchdate.second = dttime.second; + } + } + + return obj; + } + return null; + }, +} + +//TODO: Invites +/* + cal.itip.checkAndSendOrigial = cal.itip.checkAndSend; + cal.itip.checkAndSend = function(aOpType, aItem, aOriginalItem) { + //if this item is added_by_user, do not call checkAndSend yet, because the UID is wrong, we need to sync first to get the correct ID - TODO + TbSync.dump("cal.checkAndSend", aOpType); + cal.itip.checkAndSendOrigial(aOpType, aItem, aOriginalItem); + } +*/ |