pax_global_header 0000666 0000000 0000000 00000000064 14106550525 0014515 g ustar 00root root 0000000 0000000 52 comment=5f563e6c33afac948d79046f4720c63745f4c7a0
quicktext-4.1/ 0000775 0000000 0000000 00000000000 14106550525 0013402 5 ustar 00root root 0000000 0000000 quicktext-4.1/CONTRIBUTORS.md 0000664 0000000 0000000 00000000511 14106550525 0015656 0 ustar 00root root 0000000 0000000 ## Original Creator
* Emil Hesslow
## Current Maintainer
* John Bieling
## Contributors
* John Bieling
* Sebastian Haberey
* Emil Hesslow
* R Kent James
* Philippe Lieser
* Christian Schneider
* Janning Vygen
* bgeiring
* drakulis
* Peñalara Software S.L.
* Samuel Plentz
## Translators
* Alexey Sinitsyn (ru)
* Óvári (hu)
quicktext-4.1/LICENSE 0000664 0000000 0000000 00000040526 14106550525 0014416 0 ustar 00root root 0000000 0000000 Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
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/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
quicktext-4.1/README.md 0000664 0000000 0000000 00000002606 14106550525 0014665 0 ustar 00root root 0000000 0000000 [Quicktext](https://addons.thunderbird.net/addon/quicktext/) is a [Thunderbird](https://www.thunderbird.net/) extension, which has been created by Emil Hesslow. However, he was no longer able to update Quicktext or provide any kind of support. Thankfully he changed the license of Quicktext to MPL 2.0, so I could continue to update the extension and make it work with the most recent version of Thunderbird.
I will try to keep Quicktext going, but support will be limited. You may use the [issue section](https://github.com/jobisoft/quicktext/issues) of this repository to report bugs. You may also discuss Quicktext related issues with other users at https://discourse.mozilla.org/c/thunderbird.
**Note**: Emil maintained two versions of Quicktext, a free “Standard” version and a paid “Pro” version with additional features. Since he released both versions under MPL 2.0, the [official version of Quicktext](https://addons.thunderbird.net/addon/quicktext/) is based on the “Pro” version. However, the name of the extension remains just “Quicktext”. There is no longer a seperate Quicktext Pro version.
More information and usage descriptions can be found in the [wiki](https://github.com/jobisoft/quicktext/wiki) of this repository.
**The latest release and a change log can be found on [addons.thunderbird.net](https://addons.thunderbird.net/en-US/thunderbird/addon/quicktext/versions/)**.
quicktext-4.1/_locales/ 0000775 0000000 0000000 00000000000 14106550525 0015163 5 ustar 00root root 0000000 0000000 quicktext-4.1/_locales/de/ 0000775 0000000 0000000 00000000000 14106550525 0015553 5 ustar 00root root 0000000 0000000 quicktext-4.1/_locales/de/messages.json 0000664 0000000 0000000 00000000372 14106550525 0020257 0 ustar 00root root 0000000 0000000 {
"extensionDescription": {
"message": "Fügt eine Symbolleiste zum E-Mail-Bearbeitenfenster hinzu, sodass Textbausteine einfach und schnell hinzugefügt werden können. Es ist auch möglich, Variablen wie [[TO=firstname]] zu verwenden."
}
}
quicktext-4.1/_locales/en-US/ 0000775 0000000 0000000 00000000000 14106550525 0016112 5 ustar 00root root 0000000 0000000 quicktext-4.1/_locales/en-US/messages.json 0000664 0000000 0000000 00000000317 14106550525 0020615 0 ustar 00root root 0000000 0000000 {
"extensionDescription": {
"message": "Adds a toolbar with unlimited number of text to quickly insert. It is also possible to use variables like [[TO=firstname]]. With settings for everything."
}
}
quicktext-4.1/_locales/es/ 0000775 0000000 0000000 00000000000 14106550525 0015572 5 ustar 00root root 0000000 0000000 quicktext-4.1/_locales/es/messages.json 0000664 0000000 0000000 00000000345 14106550525 0020276 0 ustar 00root root 0000000 0000000 {
"extensionDescription": {
"message": "Agrega una barra de herramientas con un número ilimitado de texto para insertar rápidamente. También es posible usar variables como [[TO=firstname]]. Con ajustes para todo."
}
}
quicktext-4.1/_locales/fr/ 0000775 0000000 0000000 00000000000 14106550525 0015572 5 ustar 00root root 0000000 0000000 quicktext-4.1/_locales/fr/messages.json 0000664 0000000 0000000 00000000414 14106550525 0020273 0 ustar 00root root 0000000 0000000 {
"extensionDescription": {
"message": "Ajoute une barre d’outils à la fenêtre Email Edit afin que les modules de texte puissent être ajoutés facilement et rapidement. Il est également possible d'utiliser des variables telles que [[TO=firstname]]."
}
}
quicktext-4.1/_locales/hu/ 0000775 0000000 0000000 00000000000 14106550525 0015577 5 ustar 00root root 0000000 0000000 quicktext-4.1/_locales/hu/messages.json 0000664 0000000 0000000 00000000375 14106550525 0020306 0 ustar 00root root 0000000 0000000 {
"extensionDescription": {
"message": "Eszköztár, parancsfájlok és sablonok korlátlan lehetoségekkel, beleértve a [[TO=firstname]] változókat, a gyors szövegbeillesztéshez"
},
"extensionName": {
"message": "Gyorsszöveg"
}
}
quicktext-4.1/_locales/ja/ 0000775 0000000 0000000 00000000000 14106550525 0015555 5 ustar 00root root 0000000 0000000 quicktext-4.1/_locales/ja/messages.json 0000664 0000000 0000000 00000000317 14106550525 0020260 0 ustar 00root root 0000000 0000000 {
"extensionDescription": {
"message": "Adds a toolbar with unlimited number of text to quickly insert. It is also possible to use variables like [[TO=firstname]]. With settings for everything."
}
}
quicktext-4.1/_locales/pt_BR/ 0000775 0000000 0000000 00000000000 14106550525 0016171 5 ustar 00root root 0000000 0000000 quicktext-4.1/_locales/pt_BR/messages.json 0000664 0000000 0000000 00000000356 14106550525 0020677 0 ustar 00root root 0000000 0000000 {
"extensionDescription": {
"message": "Adiciona uma barra de ferramentas com um número ilimitado de texto para inserir rapidamente. Também é possível usar variáveis como [[TO=firstname]]. Com configurações para tudo."
}
}
quicktext-4.1/_locales/ru/ 0000775 0000000 0000000 00000000000 14106550525 0015611 5 ustar 00root root 0000000 0000000 quicktext-4.1/_locales/ru/messages.json 0000664 0000000 0000000 00000000556 14106550525 0020321 0 ustar 00root root 0000000 0000000 {
"extensionDescription": {
"message": "Добавляет панель инструментов с неограниченным количеством текста для быстрой вставки. Также можно использовать такие переменные, как [[TO=firstname]]. С настройками для всего."
}
}
quicktext-4.1/_locales/sv-SE/ 0000775 0000000 0000000 00000000000 14106550525 0016120 5 ustar 00root root 0000000 0000000 quicktext-4.1/_locales/sv-SE/messages.json 0000664 0000000 0000000 00000000317 14106550525 0020623 0 ustar 00root root 0000000 0000000 {
"extensionDescription": {
"message": "Adds a toolbar with unlimited number of text to quickly insert. It is also possible to use variables like [[TO=firstname]]. With settings for everything."
}
}
quicktext-4.1/api/ 0000775 0000000 0000000 00000000000 14106550525 0014153 5 ustar 00root root 0000000 0000000 quicktext-4.1/api/LegacyPrefs/ 0000775 0000000 0000000 00000000000 14106550525 0016357 5 ustar 00root root 0000000 0000000 quicktext-4.1/api/LegacyPrefs/README.md 0000664 0000000 0000000 00000002372 14106550525 0017642 0 ustar 00root root 0000000 0000000 ## Objective
Use this API to access Thunderbird's system preferences or to migrate your own preferences from the Thunderbird preference system to the local storage of your MailExtension.
## Usage
This API provides the following methods:
### async getPref(aName, [aFallback])
Returns the value for the ``aName`` preference. If it is not defined or has no default value assigned, ``aFallback`` will be returned (which defaults to ``null``).
### async getUserPref(aName)
Returns the user defined value for the ``aName`` preference. This will ignore any defined default value and will only return an explicitly set value, which differs from the default. Otherwise it will return ``null``.
### clearUserPref(aName)
Clears the user defined value for preference ``aName``. Subsequent calls to ``getUserPref(aName)`` will return ``null``.
### async setPref(aName, aValue)
Set the ``aName`` preference to the given value. Will return false and log an error to the console, if the type of ``aValue`` does not match the type of the preference.
---
A detailed example using the LegacyPref API to migrate add-on preferences to the local storage can be found in [/scripts/preferences/](https://github.com/thundernest/addon-developer-support/tree/master/scripts/preferences).
quicktext-4.1/api/LegacyPrefs/implementation.js 0000664 0000000 0000000 00000013374 14106550525 0021752 0 ustar 00root root 0000000 0000000 /*
* This file is provided by the addon-developer-support repository at
* https://github.com/thundernest/addon-developer-support
*
* Version: 1.7
* add onChanged event
*
* Version: 1.6
* add setDefaultPref()
*
* Version: 1.5
* replace set/getCharPref by set/getStringPref to fix encoding issue
*
* Version: 1.4
* - setPref() function returns true if the value could be set, otherwise false
*
* Version: 1.3
* - add setPref() function
*
* Version: 1.2
* - add getPref() function
*
* Author: John Bieling (john@thunderbird.net)
*
* 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/.
*/
var { ExtensionCommon } = ChromeUtils.import(
"resource://gre/modules/ExtensionCommon.jsm"
);
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
var LegacyPrefs = class extends ExtensionCommon.ExtensionAPI {
getAPI(context) {
let self = this;
this.getLegacyPref = async function (
aName,
aFallback = null,
userPrefOnly = true
) {
let prefType = Services.prefs.getPrefType(aName);
if (prefType == Services.prefs.PREF_INVALID) {
return aFallback;
}
let value = aFallback;
if (!userPrefOnly || Services.prefs.prefHasUserValue(aName)) {
switch (prefType) {
case Services.prefs.PREF_STRING:
value = Services.prefs.getStringPref(aName, aFallback);
break;
case Services.prefs.PREF_INT:
value = Services.prefs.getIntPref(aName, aFallback);
break;
case Services.prefs.PREF_BOOL:
value = Services.prefs.getBoolPref(aName, aFallback);
break;
default:
console.error(
`Legacy preference <${aName}> has an unknown type of <${prefType}>.`
);
}
}
return value;
};
this.observerTracker = null;
this.observing = false;
this.observedBranch = null;
this.observer = {
async observe(aSubject, aTopic, aData) {
if (aTopic == "nsPref:changed") {
let name = aData.substr(self.observedBranch.length);
let value = await self.getLegacyPref(aData);
self.observerTracker(name, value);
}
},
QueryInterface: ChromeUtils.generateQI([
"nsIObserver",
"nsISupportsWeakReference",
]),
};
return {
LegacyPrefs: {
onChanged: new ExtensionCommon.EventManager({
context,
name: "LegacyPrefs.onChanged",
register: (fire) => {
if (self.observing)
throw new Error("Only one onChanged observer allowed.");
if (!self.observedBranch)
throw new Error(
"Please call setObservingBranch() before using the onChanged event"
);
self.observing = true;
self.observerTracker = fire.sync;
Services.prefs
.getBranch(null)
.addObserver(self.observedBranch, self.observer);
return () => {
Services.prefs
.getBranch(null)
.removeObserver(this.observedBranch, this.observer);
self.observerTracker = null;
self.observing = false;
};
},
}).api(),
setObservingBranch: function (observeBranch) {
// This implementation only supports one observer
if (self.observing)
throw new Error(
"Already observing, cannot change observed branch."
);
if (!observeBranch) throw new Error("Invalid branch value.");
self.observedBranch = observeBranch;
},
// only returns something, if a user pref value is set
getUserPref: async function (aName) {
return await self.getLegacyPref(aName);
},
// returns the default value, if no user defined value exists,
// and returns the fallback value, if the preference does not exist
getPref: async function (aName, aFallback = null) {
return await self.getLegacyPref(aName, aFallback, false);
},
clearUserPref: function (aName) {
Services.prefs.clearUserPref(aName);
},
// sets a pref
setPref: async function (aName, aValue) {
let prefType = Services.prefs.getPrefType(aName);
if (prefType == Services.prefs.PREF_INVALID) {
console.error(
`Unknown legacy preference <${aName}>, forgot to declare a default?.`
);
return false;
}
switch (prefType) {
case Services.prefs.PREF_STRING:
Services.prefs.setStringPref(aName, aValue);
return true;
break;
case Services.prefs.PREF_INT:
Services.prefs.setIntPref(aName, aValue);
return true;
break;
case Services.prefs.PREF_BOOL:
Services.prefs.setBoolPref(aName, aValue);
return true;
break;
default:
console.error(
`Legacy preference <${aName}> has an unknown type of <${prefType}>.`
);
}
return false;
},
setDefaultPref: async function (aName, aValue) {
let defaults = Services.prefs.getDefaultBranch("");
switch (typeof aValue) {
case "string":
return defaults.setStringPref(aName, aValue);
case "number":
return defaults.setIntPref(aName, aValue);
case "boolean":
return defaults.setBoolPref(aName, aValue);
}
},
},
};
}
};
quicktext-4.1/api/LegacyPrefs/schema.json 0000664 0000000 0000000 00000007025 14106550525 0020516 0 ustar 00root root 0000000 0000000 [
{
"namespace": "LegacyPrefs",
"events": [
{
"name": "onChanged",
"type": "function",
"description": "Fired when a preference has been changed.",
"parameters": [
{
"name": "name",
"type": "string",
"description": "Name of the preference."
},
{
"name": "value",
"type": "any",
"description": "Value of the preference."
}
]
}
],
"functions": [
{
"name": "setObservingBranch",
"type": "function",
"async": true,
"description": "Set the branch which onChanged should be sensitive for.",
"parameters": [
{
"name": "aBranch",
"type": "string",
"description": "Name of the branch to observe."
}
]
},
{
"name": "getUserPref",
"type": "function",
"async": true,
"description": "Gets a user value from the legacy pref system.",
"parameters": [
{
"name": "aName",
"type": "string",
"description": "Name of the preference."
}
]
},
{
"name": "getPref",
"type": "function",
"async": true,
"description": "Gets a value from the legacy pref system.",
"parameters": [
{
"name": "aName",
"type": "string",
"description": "Name of the preference."
},
{
"name": "aFallback",
"type": "string",
"description": "Value to be returned, if the requested preference does not exist.",
"optional": true,
"default": null
}
]
},
{
"name": "setPref",
"type": "function",
"async": true,
"description": "Sets a value for an existing pref of the legacy pref system.",
"parameters": [
{
"name": "aName",
"type": "string",
"description": "Name of the preference."
},
{
"name": "aValue",
"choices": [
{
"type": "string"
},
{
"type": "integer"
},
{
"type": "boolean"
}
],
"description": "Value to be set."
}
]
},
{
"name": "setDefaultPref",
"type": "function",
"async": true,
"description": "Defines the default value for pref of the legacy pref system. This defines the type of a pref and is needed for new prefs.",
"parameters": [
{
"name": "aName",
"type": "string",
"description": "Name of the preference."
},
{
"name": "aValue",
"choices": [
{
"type": "string"
},
{
"type": "integer"
},
{
"type": "boolean"
}
],
"description": "Default value to be set."
}
]
},
{
"name": "clearUserPref",
"type": "function",
"description": "Removes a user value from the legacy pref system.",
"parameters": [
{
"name": "aName",
"type": "string",
"description": "Name of the preference."
}
]
}
]
}
]
quicktext-4.1/api/NotifyTools/ 0000775 0000000 0000000 00000000000 14106550525 0016444 5 ustar 00root root 0000000 0000000 quicktext-4.1/api/NotifyTools/README.md 0000664 0000000 0000000 00000010523 14106550525 0017724 0 ustar 00root root 0000000 0000000 # Objective
The NotifyTools provide a bidirectional messaging system between Experiments scripts and the WebExtension's background page (even with [e10s](https://developer.thunderbird.net/add-ons/updating/tb91/changes#thunderbird-is-now-multi-process-e-10-s) being enabled in Thunderbird Beta 86).

They allow to work on add-on uprades in smaller steps, as single calls (like `window.openDialog()`)
in the middle of legacy code can be replaced by WebExtension calls, by stepping out of the Experiment
and back in when the task has been finished.
More details can be found in [this update tutorial](https://github.com/thundernest/addon-developer-support/wiki/Tutorial:-Convert-add-on-parts-individually-by-using-a-messaging-system).
# Usage
Add the [NotifyTools API](https://github.com/thundernest/addon-developer-support/tree/master/auxiliary-apis/NotifyTools) to your add-on. Your `manifest.json` needs an entry like this:
```
"experiment_apis": {
"NotifyTools": {
"schema": "api/NotifyTools/schema.json",
"parent": {
"scopes": ["addon_parent"],
"paths": [["NotifyTools"]],
"script": "api/NotifyTools/implementation.js"
}
}
},
```
Additionally to the [NotifyTools API](https://github.com/thundernest/addon-developer-support/tree/master/auxiliary-apis/NotifyTools) the [notifyTools.js](https://github.com/thundernest/addon-developer-support/tree/master/scripts/notifyTools) script is needed as the counterpart in Experiment scripts.
**Note:** You need to adjust the `notifyTools.js` script and add your add-on ID at the top.
## Receiving notifications from Experiment scripts
Add a listener for the `onNotifyBackground` event in your WebExtension's background page:
```
messenger.NotifyTools.onNotifyBackground.addListener(async (info) => {
switch (info.command) {
case "doSomething":
//do something
let rv = await doSomething();
return rv;
break;
}
});
```
The `onNotifyBackground` event will receive and respond to notifications send from your Experiment scripts:
```
notifyTools.notifyBackground({command: "doSomething"}).then((data) => {
console.log(data);
});
```
Include the [notifyTools.js](https://github.com/thundernest/addon-developer-support/tree/master/scripts/notifyTools) script in your Experiment script to be able to use `notifyTools.notifyBackground()`.
**Note**: If multiple `onNotifyBackground` listeners are registered in the WebExtension's background page and more than one is returning data, the value
from the first one is returned to the Experiment. This may lead to inconsistent behavior, so make sure that for each
request only one listener is returning data.
## Sending notifications to Experiments scripts
Use the `notifyExperiment()` method to send a notification from the WebExtension's background page to Experiment scripts:
```
messenger.NotifyTools.notifyExperiment({command: "doSomething"}).then((data) => {
console.log(data)
});
```
The receiving Experiment script needs to include the [notifyTools.js](https://github.com/thundernest/addon-developer-support/tree/master/scripts/notifyTools) script and must setup a listener using the following methods:
### registerListener(callback);
Registers a callback function, which is called when a notification from the WebExtension's background page has been received. The `registerListener()` function returns an `id` which can be used to remove the listener again.
Example:
```
function doSomething(data) {
console.log(data);
return true;
}
let id = notifyTools.registerListener(doSomething);
```
### removeListener(id)
Removes the listener with the given `id`.
Example:
```
notifyTools.removeListener(id);
```
### enable()
The [notifyTools.js](https://github.com/thundernest/addon-developer-support/tree/master/scripts/notifyTools) script attaches its `enable()` method to the `load` event of the current window. If the script is loaded into a window-less environment, `enable()` needs to be called manually.
### disable()
The [notifyTools.js](https://github.com/thundernest/addon-developer-support/tree/master/scripts/notifyTools) script attaches its `disable()` method to the `unload` event of the current window. If the script is loaded into a window-less environment, `disable()` needs to be called manually.
quicktext-4.1/api/NotifyTools/implementation.js 0000664 0000000 0000000 00000007412 14106550525 0022033 0 ustar 00root root 0000000 0000000 /*
* This file is provided by the addon-developer-support repository at
* https://github.com/thundernest/addon-developer-support
*
* Author: John Bieling (john@thunderbird.net)
*
* 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/.
*/
// Get various parts of the WebExtension framework that we need.
var { ExtensionCommon } = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
var { ExtensionSupport } = ChromeUtils.import("resource:///modules/ExtensionSupport.jsm");
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
var NotifyTools = class extends ExtensionCommon.ExtensionAPI {
getAPI(context) {
var self = this;
this.onNotifyBackgroundObserver = {
observe: async function (aSubject, aTopic, aData) {
if (
Object.keys(self.observerTracker).length > 0 &&
aData == self.extension.id
) {
let payload = aSubject.wrappedJSObject;
// This is called from the BL observer.js and therefore it should have a resolve
// payload, but better check.
if (payload.resolve) {
let observerTrackerPromises = [];
// Push listener into promise array, so they can run in parallel
for (let listener of Object.values(self.observerTracker)) {
observerTrackerPromises.push(listener(payload.data));
}
// We still have to await all of them but wait time is just the time needed
// for the slowest one.
let results = [];
for (let observerTrackerPromise of observerTrackerPromises) {
let rv = await observerTrackerPromise;
if (rv != null) results.push(rv);
}
if (results.length == 0) {
payload.resolve();
} else {
if (results.length > 1) {
console.warn(
"Received multiple results from onNotifyBackground listeners. Using the first one, which can lead to inconsistent behavior.",
results
);
}
payload.resolve(results[0]);
}
} else {
// Just call the listener.
for (let listener of Object.values(self.observerTracker)) {
listener(payload.data);
}
}
}
},
};
this.observerTracker = {};
this.observerTrackerNext = 1;
// Add observer for notifyTools.js
Services.obs.addObserver(
this.onNotifyBackgroundObserver,
"NotifyBackgroundObserver",
false
);
return {
NotifyTools: {
notifyExperiment(data) {
return new Promise(resolve => {
Services.obs.notifyObservers(
{ data, resolve },
"NotifyExperimentObserver",
self.extension.id
);
});
},
onNotifyBackground: new ExtensionCommon.EventManager({
context,
name: "NotifyTools.onNotifyBackground",
register: (fire) => {
let trackerId = self.observerTrackerNext++;
self.observerTracker[trackerId] = fire.sync;
return () => {
delete self.observerTracker[trackerId];
};
},
}).api(),
}
};
}
onShutdown(isAppShutdown) {
if (isAppShutdown) {
return; // the application gets unloaded anyway
}
// Remove observer for notifyTools.js
Services.obs.removeObserver(
this.onNotifyBackgroundObserver,
"NotifyBackgroundObserver"
);
// Flush all caches
Services.obs.notifyObservers(null, "startupcache-invalidate");
}
};
quicktext-4.1/api/NotifyTools/schema.json 0000664 0000000 0000000 00000001544 14106550525 0020603 0 ustar 00root root 0000000 0000000 [
{
"namespace": "NotifyTools",
"events": [
{
"name": "onNotifyBackground",
"type": "function",
"description": "Fired when a new notification from notifyTools.js in an Experiment has been received.",
"parameters": [
{
"name": "data",
"type": "any",
"description": "Restrictions of the structured clone algorithm apply."
}
]
}
],
"functions": [
{
"name": "notifyExperiment",
"type": "function",
"async": true,
"description": "Notifies notifyTools.js in an Experiment and sends data.",
"parameters": [
{
"name": "data",
"type": "any",
"description": "Restrictions of the structured clone algorithm apply."
}
]
}
]
}
]
quicktext-4.1/api/WindowListener/ 0000775 0000000 0000000 00000000000 14106550525 0017130 5 ustar 00root root 0000000 0000000 quicktext-4.1/api/WindowListener/CHANGELOG.md 0000664 0000000 0000000 00000005545 14106550525 0020752 0 ustar 00root root 0000000 0000000 Version: 1.54
-------------
- fix "ownerDoc.getElementById() is undefined" bug
Version: 1.53
-------------
- fix "tab.browser is undefined" bug
Version: 1.52
-------------
- clear cache only if add-on is uninstalled/updated, not on app shutdown
Version: 1.51
-------------
- use wrench button for options for TB78.10
Version: 1.50
-------------
- use built-in CSS rules to fix options button for dark themes (thanks to Thunder)
- fix some occasions where options button was not added
Version: 1.49
-------------
- fixed missing eventListener for Beta + Daily
Version: 1.48
-------------
- moved notifyTools into its own NotifyTools API.
Version: 1.39
-------------
- fix for 68
Version: 1.36
-------------
- fix for beta 87
Version: 1.35
-------------
- add support for options button/menu in add-on manager and fix 68 double menu entry
Version: 1.34
-------------
- fix error in unload
Version: 1.33
-------------
- fix for e10s
Version: 1.30
-------------
- replace setCharPref by setStringPref to cope with UTF-8 encoding
Version: 1.29
-------------
- open options window centered
Version: 1.28
-------------
- do not crash on missing icon
Version: 1.27
-------------
- add openOptionsDialog()
Version: 1.26
-------------
- pass WL object to legacy preference window
Version: 1.25
-------------
- adding waitForMasterPassword
Version: 1.24
-------------
- automatically localize i18n locale strings in injectElements()
Version: 1.22
-------------
- to reduce confusions, only check built-in URLs as add-on URLs cannot
be resolved if a temp installed add-on has bin zipped
Version: 1.21
-------------
- print debug messages only if add-ons are installed temporarily from
the add-on debug page
- add checks to registered windows and scripts, if they actually exists
Version: 1.20
-------------
- fix long delay before customize window opens
- fix non working removal of palette items
Version: 1.19
-------------
- add support for ToolbarPalette
Version: 1.18
-------------
- execute shutdown script also during global app shutdown (fixed)
Version: 1.17
-------------
- execute shutdown script also during global app shutdown
Version: 1.16
-------------
- support for persist
Version: 1.15
-------------
- make (undocumented) startup() async
Version: 1.14
-------------
- support resource urls
Version: 1.12
-------------
- no longer allow to enforce custom "namespace"
- no longer call it namespace but uniqueRandomID / scopeName
- expose special objects as the global WL object
- autoremove injected elements after onUnload has ben executed
Version: 1.9
-------------
- automatically remove all entries added by injectElements
Version: 1.8
-------------
- add injectElements
Version: 1.7
-------------
- add injectCSS
- add optional enforced namespace
Version: 1.6
-------------
- added mutation observer to be able to inject into browser elements
- use larger icons as fallback
quicktext-4.1/api/WindowListener/README.md 0000664 0000000 0000000 00000000331 14106550525 0020404 0 ustar 00root root 0000000 0000000 Usage description can be found in the [wiki](https://github.com/thundernest/addon-developer-support/wiki/Using-the-WindowListener-API-to-convert-a-Legacy-Overlay-WebExtension-into-a-MailExtension-for-Thunderbird-78).
quicktext-4.1/api/WindowListener/implementation.js 0000664 0000000 0000000 00000123122 14106550525 0022514 0 ustar 00root root 0000000 0000000 /*
* This file is provided by the addon-developer-support repository at
* https://github.com/thundernest/addon-developer-support
*
* Version: 1.54
*
* Author: John Bieling (john@thunderbird.net)
*
* 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/.
*/
// Import some things we need.
var { ExtensionCommon } = ChromeUtils.import(
"resource://gre/modules/ExtensionCommon.jsm"
);
var { ExtensionSupport } = ChromeUtils.import(
"resource:///modules/ExtensionSupport.jsm"
);
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
var WindowListener = class extends ExtensionCommon.ExtensionAPI {
log(msg) {
if (this.debug) console.log("WindowListener API: " + msg);
}
getThunderbirdVersion() {
let parts = Services.appinfo.version.split(".");
return {
major: parseInt(parts[0]),
minor: parseInt(parts[1]),
}
}
getCards(e) {
// This gets triggered by real events but also manually by providing the outer window.
// The event is attached to the outer browser, get the inner one.
let doc;
// 78,86, and 87+ need special handholding. *Yeah*.
if (this.getThunderbirdVersion().major < 86) {
let ownerDoc = e.document || e.target.ownerDocument;
doc = ownerDoc.getElementById("html-view-browser").contentDocument;
} else if (this.getThunderbirdVersion().major < 87) {
let ownerDoc = e.document || e.target;
doc = ownerDoc.getElementById("html-view-browser").contentDocument;
} else {
doc = e.document || e.target;
}
return doc.querySelectorAll("addon-card");
}
// Add pref entry to 68
add68PrefsEntry(event) {
let id = this.menu_addonPrefs_id + "_" + this.uniqueRandomID;
// Get the best size of the icon (16px or bigger)
let iconSizes = this.extension.manifest.icons
? Object.keys(this.extension.manifest.icons)
: [];
iconSizes.sort((a, b) => a - b);
let bestSize = iconSizes.filter((e) => parseInt(e) >= 16).shift();
let icon = bestSize ? this.extension.manifest.icons[bestSize] : "";
let name = this.extension.manifest.name;
let entry = icon
? event.target.ownerGlobal.MozXULElement.parseXULToFragment(
``
)
: event.target.ownerGlobal.MozXULElement.parseXULToFragment(
``
);
event.target.appendChild(entry);
let noPrefsElem = event.target.querySelector('[disabled="true"]');
// using collapse could be undone by core, so we use display none
// noPrefsElem.setAttribute("collapsed", "true");
noPrefsElem.style.display = "none";
event.target.ownerGlobal.document
.getElementById(id)
.addEventListener("command", this);
}
// Event handler for the addon manager, to update the state of the options button.
handleEvent(e) {
switch (e.type) {
// 68 add-on options menu showing
case "popupshowing":
{
this.add68PrefsEntry(e);
}
break;
// 78/88 add-on options menu/button click
case "click":
{
e.preventDefault();
e.stopPropagation();
let WL = {};
WL.extension = this.extension;
WL.messenger = this.getMessenger(this.context);
let w = Services.wm.getMostRecentWindow("mail:3pane");
w.openDialog(
this.pathToOptionsPage,
"AddonOptions",
"chrome,resizable,centerscreen",
WL
);
}
break;
// 68 add-on options menu command
case "command":
{
let WL = {};
WL.extension = this.extension;
WL.messenger = this.getMessenger(this.context);
e.target.ownerGlobal.openDialog(
this.pathToOptionsPage,
"AddonOptions",
"chrome,resizable,centerscreen",
WL
);
}
break;
// update, ViewChanged and manual call for add-on manager options overlay
default: {
let cards = this.getCards(e);
for (let card of cards) {
// Setup either the options entry in the menu or the button
//window.document.getElementById(id).addEventListener("command", function() {window.openDialog(self.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", WL)});
if (card.addon.id == this.extension.id) {
let optionsMenu =
(this.getThunderbirdVersion().major > 78 && this.getThunderbirdVersion().major < 88) ||
(this.getThunderbirdVersion().major == 78 && this.getThunderbirdVersion().minor < 10);
if (optionsMenu) {
// Options menu in 78.0-78.10 and 79-87
let addonOptionsLegacyEntry = card.querySelector(
".extension-options-legacy"
);
if (card.addon.isActive && !addonOptionsLegacyEntry) {
let addonOptionsEntry = card.querySelector(
"addon-options panel-list panel-item[action='preferences']"
);
addonOptionsLegacyEntry = card.ownerDocument.createElement(
"panel-item"
);
addonOptionsLegacyEntry.setAttribute(
"data-l10n-id",
"preferences-addon-button"
);
addonOptionsLegacyEntry.classList.add(
"extension-options-legacy"
);
addonOptionsEntry.parentNode.insertBefore(
addonOptionsLegacyEntry,
addonOptionsEntry
);
card
.querySelector(".extension-options-legacy")
.addEventListener("click", this);
} else if (!card.addon.isActive && addonOptionsLegacyEntry) {
addonOptionsLegacyEntry.remove();
}
} else {
// Add-on button in 88
let addonOptionsButton = card.querySelector(
".windowlistener-options-button"
);
if (card.addon.isActive && !addonOptionsButton) {
addonOptionsButton = card.ownerDocument.createElement("button");
addonOptionsButton.classList.add("windowlistener-options-button");
addonOptionsButton.classList.add("extension-options-button");
card.optionsButton.parentNode.insertBefore(
addonOptionsButton,
card.optionsButton
);
card
.querySelector(".windowlistener-options-button")
.addEventListener("click", this);
} else if (!card.addon.isActive && addonOptionsButton) {
addonOptionsButton.remove();
}
}
}
}
}
}
}
// Some tab/add-on-manager related functions
getTabMail(window) {
return window.document.getElementById("tabmail");
}
// returns the outer browser, not the nested browser of the add-on manager
// events must be attached to the outer browser
getAddonManagerFromTab(tab) {
if (tab.browser) {
let win = tab.browser.contentWindow;
if (win && win.location.href == "about:addons") {
return win;
}
}
}
getAddonManagerFromWindow(window) {
let tabMail = this.getTabMail(window);
for (let tab of tabMail.tabInfo) {
let win = this.getAddonManagerFromTab(tab);
if (win) {
return win;
}
}
}
setupAddonManager(managerWindow, forceLoad = false) {
if (!managerWindow) {
return;
}
if (!(
managerWindow &&
managerWindow[this.uniqueRandomID] &&
managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners
)) {
managerWindow.document.addEventListener("ViewChanged", this);
managerWindow.document.addEventListener("update", this);
managerWindow.document.addEventListener("view-loaded", this);
managerWindow[this.uniqueRandomID] = {};
managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = true;
}
if (forceLoad) this.handleEvent(managerWindow);
}
getMessenger(context) {
let apis = ["storage", "runtime", "extension", "i18n"];
function getStorage() {
let localstorage = null;
try {
localstorage = context.apiCan.findAPIPath("storage");
localstorage.local.get = (...args) =>
localstorage.local.callMethodInParentProcess("get", args);
localstorage.local.set = (...args) =>
localstorage.local.callMethodInParentProcess("set", args);
localstorage.local.remove = (...args) =>
localstorage.local.callMethodInParentProcess("remove", args);
localstorage.local.clear = (...args) =>
localstorage.local.callMethodInParentProcess("clear", args);
} catch (e) {
console.info("Storage permission is missing");
}
return localstorage;
}
let messenger = {};
for (let api of apis) {
switch (api) {
case "storage":
XPCOMUtils.defineLazyGetter(messenger, "storage", () => getStorage());
break;
default:
XPCOMUtils.defineLazyGetter(messenger, api, () =>
context.apiCan.findAPIPath(api)
);
}
}
return messenger;
}
error(msg) {
if (this.debug) console.error("WindowListener API: " + msg);
}
// async sleep function using Promise
async sleep(delay) {
let timer = Components.classes["@mozilla.org/timer;1"].createInstance(
Components.interfaces.nsITimer
);
return new Promise(function (resolve, reject) {
let event = {
notify: function (timer) {
resolve();
},
};
timer.initWithCallback(
event,
delay,
Components.interfaces.nsITimer.TYPE_ONE_SHOT
);
});
}
getAPI(context) {
// Track if this is the background/main context
if (context.viewType != "background")
throw new Error(
"The WindowListener API may only be called from the background page."
);
this.context = context;
this.uniqueRandomID = "AddOnNS" + context.extension.instanceId;
this.menu_addonPrefs_id = "addonPrefs";
this.registeredWindows = {};
this.pathToStartupScript = null;
this.pathToShutdownScript = null;
this.pathToOptionsPage = null;
this.chromeHandle = null;
this.chromeData = null;
this.resourceData = null;
this.openWindows = [];
this.debug = context.extension.addonData.temporarilyInstalled;
const aomStartup = Cc[
"@mozilla.org/addons/addon-manager-startup;1"
].getService(Ci.amIAddonManagerStartup);
const resProto = Cc[
"@mozilla.org/network/protocol;1?name=resource"
].getService(Ci.nsISubstitutingProtocolHandler);
let self = this;
// TabMonitor to detect opening of tabs, to setup the options button in the add-on manager.
this.tabMonitor = {
onTabTitleChanged(aTab) {},
onTabClosing(aTab) {},
onTabPersist(aTab) {},
onTabRestored(aTab) {},
onTabSwitched(aNewTab, aOldTab) {
//self.setupAddonManager(self.getAddonManagerFromTab(aNewTab));
},
async onTabOpened(aTab) {
if (aTab.browser) {
if (!aTab.pageLoaded) {
// await a location change if browser is not loaded yet
await new Promise((resolve) => {
let reporterListener = {
QueryInterface: ChromeUtils.generateQI([
"nsIWebProgressListener",
"nsISupportsWeakReference",
]),
onStateChange() {},
onProgressChange() {},
onLocationChange(
/* in nsIWebProgress*/ aWebProgress,
/* in nsIRequest*/ aRequest,
/* in nsIURI*/ aLocation
) {
aTab.browser.removeProgressListener(reporterListener);
resolve();
},
onStatusChange() {},
onSecurityChange() {},
onContentBlockingEvent() {},
};
aTab.browser.addProgressListener(reporterListener);
});
}
self.setupAddonManager(self.getAddonManagerFromTab(aTab));
}
},
};
return {
WindowListener: {
async waitForMasterPassword() {
// Wait until master password has been entered (if needed)
while (!Services.logins.isLoggedIn) {
self.log("Waiting for master password.");
await self.sleep(1000);
}
self.log("Master password has been entered.");
},
aDocumentExistsAt(uriString) {
self.log(
"Checking if document at <" +
uriString +
"> used in registration actually exists."
);
try {
let uriObject = Services.io.newURI(uriString);
let content = Cu.readUTF8URI(uriObject);
} catch (e) {
Components.utils.reportError(e);
return false;
}
return true;
},
registerOptionsPage(optionsUrl) {
self.pathToOptionsPage = optionsUrl.startsWith("chrome://")
? optionsUrl
: context.extension.rootURI.resolve(optionsUrl);
},
registerDefaultPrefs(defaultUrl) {
let url = context.extension.rootURI.resolve(defaultUrl);
let prefsObj = {};
prefsObj.Services = ChromeUtils.import(
"resource://gre/modules/Services.jsm"
).Services;
prefsObj.pref = function (aName, aDefault) {
let defaults = Services.prefs.getDefaultBranch("");
switch (typeof aDefault) {
case "string":
return defaults.setStringPref(aName, aDefault);
case "number":
return defaults.setIntPref(aName, aDefault);
case "boolean":
return defaults.setBoolPref(aName, aDefault);
default:
throw new Error(
"Preference <" +
aName +
"> has an unsupported type <" +
typeof aDefault +
">. Allowed are string, number and boolean."
);
}
};
Services.scriptloader.loadSubScript(url, prefsObj, "UTF-8");
},
registerChromeUrl(data) {
let chromeData = [];
let resourceData = [];
for (let entry of data) {
if (entry[0] == "resource") resourceData.push(entry);
else chromeData.push(entry);
}
if (chromeData.length > 0) {
const manifestURI = Services.io.newURI(
"manifest.json",
null,
context.extension.rootURI
);
self.chromeHandle = aomStartup.registerChrome(
manifestURI,
chromeData
);
}
for (let res of resourceData) {
// [ "resource", "shortname" , "path" ]
let uri = Services.io.newURI(
res[2],
null,
context.extension.rootURI
);
resProto.setSubstitutionWithFlags(
res[1],
uri,
resProto.ALLOW_CONTENT_ACCESS
);
}
self.chromeData = chromeData;
self.resourceData = resourceData;
},
registerWindow(windowHref, jsFile) {
if (self.debug && !this.aDocumentExistsAt(windowHref)) {
self.error(
"Attempt to register an injector script for non-existent window: " +
windowHref
);
return;
}
if (!self.registeredWindows.hasOwnProperty(windowHref)) {
// path to JS file can either be chrome:// URL or a relative URL
let path = jsFile.startsWith("chrome://")
? jsFile
: context.extension.rootURI.resolve(jsFile);
self.registeredWindows[windowHref] = path;
} else {
self.error(
"Window <" + windowHref + "> has already been registered"
);
}
},
registerStartupScript(aPath) {
self.pathToStartupScript = aPath.startsWith("chrome://")
? aPath
: context.extension.rootURI.resolve(aPath);
},
registerShutdownScript(aPath) {
self.pathToShutdownScript = aPath.startsWith("chrome://")
? aPath
: context.extension.rootURI.resolve(aPath);
},
openOptionsDialog(windowId) {
let window = context.extension.windowManager.get(windowId, context)
.window;
let WL = {};
WL.extension = self.extension;
WL.messenger = self.getMessenger(self.context);
window.openDialog(
self.pathToOptionsPage,
"AddonOptions",
"chrome,resizable,centerscreen",
WL
);
},
async startListening() {
// load the registered startup script, if one has been registered
// (mail3:pane may not have been fully loaded yet)
if (self.pathToStartupScript) {
let startupJS = {};
startupJS.WL = {};
startupJS.WL.extension = self.extension;
startupJS.WL.messenger = self.getMessenger(self.context);
try {
if (self.pathToStartupScript) {
Services.scriptloader.loadSubScript(
self.pathToStartupScript,
startupJS,
"UTF-8"
);
// delay startup until startup has been finished
self.log(
"Waiting for async startup() in <" +
self.pathToStartupScript +
"> to finish."
);
if (startupJS.startup) {
await startupJS.startup();
self.log(
"startup() in <" + self.pathToStartupScript + "> finished"
);
} else {
self.log(
"No startup() in <" + self.pathToStartupScript + "> found."
);
}
}
} catch (e) {
Components.utils.reportError(e);
}
}
let urls = Object.keys(self.registeredWindows);
if (urls.length > 0) {
// Before registering the window listener, check which windows are already open
self.openWindows = [];
for (let window of Services.wm.getEnumerator(null)) {
self.openWindows.push(window);
}
// Register window listener for all pre-registered windows
ExtensionSupport.registerWindowListener(
"injectListener_" + self.uniqueRandomID,
{
// React on all windows and manually reduce to the registered
// windows, so we can do special actions when the main
// messenger window is opened.
//chromeURLs: Object.keys(self.registeredWindows),
async onLoadWindow(window) {
// Create add-on scope
window[self.uniqueRandomID] = {};
// Special action #1: If this is the main messenger window
if (
window.location.href ==
"chrome://messenger/content/messenger.xul" ||
window.location.href ==
"chrome://messenger/content/messenger.xhtml"
) {
if (self.pathToOptionsPage) {
if (self.getThunderbirdVersion().major < 78) {
let element_addonPrefs = window.document.getElementById(
self.menu_addonPrefs_id
);
element_addonPrefs.addEventListener(
"popupshowing",
self
);
} else {
// Setup the options button/menu in the add-on manager, if it is already open.
self.setupAddonManager(
self.getAddonManagerFromWindow(window),
true
);
// Add a tabmonitor, to be able to setup the options button/menu in the add-on manager.
self
.getTabMail(window)
.registerTabMonitor(self.tabMonitor);
window[self.uniqueRandomID].hasTabMonitor = true;
}
}
}
// Special action #2: If this page contains browser elements
let browserElements = window.document.getElementsByTagName(
"browser"
);
if (browserElements.length > 0) {
//register a MutationObserver
window[
self.uniqueRandomID
]._mObserver = new window.MutationObserver(function (
mutations
) {
mutations.forEach(async function (mutation) {
if (
mutation.attributeName == "src" &&
self.registeredWindows.hasOwnProperty(
mutation.target.getAttribute("src")
)
) {
// When the MutationObserver callsback, the window is still showing "about:black" and it is going
// to unload and then load the new page. Any eventListener attached to the window will be removed
// so we cannot listen for the load event. We have to poll manually to learn when loading has finished.
// On my system it takes 70ms.
let loaded = false;
for (let i = 0; i < 100 && !loaded; i++) {
await self.sleep(100);
let targetWindow =
mutation.target.contentWindow.wrappedJSObject;
if (
targetWindow &&
targetWindow.location.href ==
mutation.target.getAttribute("src") &&
targetWindow.document.readyState == "complete"
) {
loaded = true;
break;
}
}
if (loaded) {
let targetWindow =
mutation.target.contentWindow.wrappedJSObject;
// Create add-on scope
targetWindow[self.uniqueRandomID] = {};
// Inject with isAddonActivation = false
self._loadIntoWindow(targetWindow, false);
}
}
});
});
for (let element of browserElements) {
if (
self.registeredWindows.hasOwnProperty(
element.getAttribute("src")
)
) {
let targetWindow =
element.contentWindow.wrappedJSObject;
// Create add-on scope
targetWindow[self.uniqueRandomID] = {};
// Inject with isAddonActivation = true
self._loadIntoWindow(targetWindow, true);
} else {
// Window/Browser is not yet fully loaded, postpone injection via MutationObserver
window[self.uniqueRandomID]._mObserver.observe(
element,
{
attributes: true,
childList: false,
characterData: false,
}
);
}
}
}
// Load JS into window
self._loadIntoWindow(
window,
self.openWindows.includes(window)
);
},
onUnloadWindow(window) {
// Remove JS from window, window is being closed, addon is not shut down
self._unloadFromWindow(window, false);
},
}
);
} else {
self.error("Failed to start listening, no windows registered");
}
},
},
};
}
_loadIntoWindow(window, isAddonActivation) {
if (
window.hasOwnProperty(this.uniqueRandomID) &&
this.registeredWindows.hasOwnProperty(window.location.href)
) {
try {
let uniqueRandomID = this.uniqueRandomID;
let extension = this.extension;
// Add reference to window to add-on scope
window[this.uniqueRandomID].window = window;
window[this.uniqueRandomID].document = window.document;
// Keep track of toolbarpalettes we are injecting into
window[this.uniqueRandomID]._toolbarpalettes = {};
//Create WLDATA object
window[this.uniqueRandomID].WL = {};
window[this.uniqueRandomID].WL.scopeName = this.uniqueRandomID;
// Add helper function to inject CSS to WLDATA object
window[this.uniqueRandomID].WL.injectCSS = function (cssFile) {
let element;
let v = parseInt(Services.appinfo.version.split(".").shift());
// using createElementNS in TB78 delays the insert process and hides any security violation errors
if (v > 68) {
element = window.document.createElement("link");
} else {
let ns = window.document.documentElement.lookupNamespaceURI("html");
element = window.document.createElementNS(ns, "link");
}
element.setAttribute("wlapi_autoinjected", uniqueRandomID);
element.setAttribute("rel", "stylesheet");
element.setAttribute("href", cssFile);
return window.document.documentElement.appendChild(element);
};
// Add helper function to inject XUL to WLDATA object
window[this.uniqueRandomID].WL.injectElements = function (
xulString,
dtdFiles = [],
debug = false
) {
let toolbarsToResolve = [];
function checkElements(stringOfIDs) {
let arrayOfIDs = stringOfIDs.split(",").map((e) => e.trim());
for (let id of arrayOfIDs) {
let element = window.document.getElementById(id);
if (element) {
return element;
}
}
return null;
}
function localize(entity) {
let msg = entity.slice("__MSG_".length, -2);
return extension.localeData.localizeMessage(msg);
}
function injectChildren(elements, container) {
if (debug) console.log(elements);
for (let i = 0; i < elements.length; i++) {
// take care of persists
const uri = window.document.documentURI;
for (const persistentNode of elements[i].querySelectorAll(
"[persist]"
)) {
for (const persistentAttribute of persistentNode
.getAttribute("persist")
.trim()
.split(" ")) {
if (
Services.xulStore.hasValue(
uri,
persistentNode.id,
persistentAttribute
)
) {
persistentNode.setAttribute(
persistentAttribute,
Services.xulStore.getValue(
uri,
persistentNode.id,
persistentAttribute
)
);
}
}
}
if (
elements[i].hasAttribute("insertafter") &&
checkElements(elements[i].getAttribute("insertafter"))
) {
let insertAfterElement = checkElements(
elements[i].getAttribute("insertafter")
);
if (debug)
console.log(
elements[i].tagName +
"#" +
elements[i].id +
": insertafter " +
insertAfterElement.id
);
if (
debug &&
elements[i].id &&
window.document.getElementById(elements[i].id)
) {
console.error(
"The id <" +
elements[i].id +
"> of the injected element already exists in the document!"
);
}
elements[i].setAttribute("wlapi_autoinjected", uniqueRandomID);
insertAfterElement.parentNode.insertBefore(
elements[i],
insertAfterElement.nextSibling
);
} else if (
elements[i].hasAttribute("insertbefore") &&
checkElements(elements[i].getAttribute("insertbefore"))
) {
let insertBeforeElement = checkElements(
elements[i].getAttribute("insertbefore")
);
if (debug)
console.log(
elements[i].tagName +
"#" +
elements[i].id +
": insertbefore " +
insertBeforeElement.id
);
if (
debug &&
elements[i].id &&
window.document.getElementById(elements[i].id)
) {
console.error(
"The id <" +
elements[i].id +
"> of the injected element already exists in the document!"
);
}
elements[i].setAttribute("wlapi_autoinjected", uniqueRandomID);
insertBeforeElement.parentNode.insertBefore(
elements[i],
insertBeforeElement
);
} else if (
elements[i].id &&
window.document.getElementById(elements[i].id)
) {
// existing container match, dive into recursivly
if (debug)
console.log(
elements[i].tagName +
"#" +
elements[i].id +
" is an existing container, injecting into " +
elements[i].id
);
injectChildren(
Array.from(elements[i].children),
window.document.getElementById(elements[i].id)
);
} else if (elements[i].localName === "toolbarpalette") {
// These vanish from the document but still exist via the palette property
if (debug) console.log(elements[i].id + " is a toolbarpalette");
let boxes = [
...window.document.getElementsByTagName("toolbox"),
];
let box = boxes.find(
(box) => box.palette && box.palette.id === elements[i].id
);
let palette = box ? box.palette : null;
if (!palette) {
if (debug)
console.log(
`The palette for ${elements[i].id} could not be found, deferring to later`
);
continue;
}
if (debug)
console.log(`The toolbox for ${elements[i].id} is ${box.id}`);
toolbarsToResolve.push(...box.querySelectorAll("toolbar"));
toolbarsToResolve.push(
...window.document.querySelectorAll(
`toolbar[toolboxid="${box.id}"]`
)
);
for (let child of elements[i].children) {
child.setAttribute("wlapi_autoinjected", uniqueRandomID);
}
window[uniqueRandomID]._toolbarpalettes[palette.id] = palette;
injectChildren(Array.from(elements[i].children), palette);
} else {
// append element to the current container
if (debug)
console.log(
elements[i].tagName +
"#" +
elements[i].id +
": append to " +
container.id
);
elements[i].setAttribute("wlapi_autoinjected", uniqueRandomID);
container.appendChild(elements[i]);
}
}
}
if (debug) console.log("Injecting into root document:");
let localizedXulString = xulString.replace(
/__MSG_(.*?)__/g,
localize
);
injectChildren(
Array.from(
window.MozXULElement.parseXULToFragment(
localizedXulString,
dtdFiles
).children
),
window.document.documentElement
);
for (let bar of toolbarsToResolve) {
let currentset = Services.xulStore.getValue(
window.location,
bar.id,
"currentset"
);
if (currentset) {
bar.currentSet = currentset;
} else if (bar.getAttribute("defaultset")) {
bar.currentSet = bar.getAttribute("defaultset");
}
}
};
// Add extension object to WLDATA object
window[this.uniqueRandomID].WL.extension = this.extension;
// Add messenger object to WLDATA object
window[this.uniqueRandomID].WL.messenger = this.getMessenger(
this.context
);
// Load script into add-on scope
Services.scriptloader.loadSubScript(
this.registeredWindows[window.location.href],
window[this.uniqueRandomID],
"UTF-8"
);
window[this.uniqueRandomID].onLoad(isAddonActivation);
} catch (e) {
Components.utils.reportError(e);
}
}
}
_unloadFromWindow(window, isAddonDeactivation) {
// unload any contained browser elements
if (
window.hasOwnProperty(this.uniqueRandomID) &&
window[this.uniqueRandomID].hasOwnProperty("_mObserver")
) {
window[this.uniqueRandomID]._mObserver.disconnect();
let browserElements = window.document.getElementsByTagName("browser");
for (let element of browserElements) {
if (element.contentWindow) {
this._unloadFromWindow(
element.contentWindow.wrappedJSObject,
isAddonDeactivation
);
}
}
}
if (
window.hasOwnProperty(this.uniqueRandomID) &&
this.registeredWindows.hasOwnProperty(window.location.href)
) {
// Remove this window from the list of open windows
this.openWindows = this.openWindows.filter((e) => e != window);
if (window[this.uniqueRandomID].onUnload) {
try {
// Call onUnload()
window[this.uniqueRandomID].onUnload(isAddonDeactivation);
} catch (e) {
Components.utils.reportError(e);
}
}
// Remove all auto injected objects
let elements = Array.from(
window.document.querySelectorAll(
'[wlapi_autoinjected="' + this.uniqueRandomID + '"]'
)
);
for (let element of elements) {
element.remove();
}
// Remove all autoinjected toolbarpalette items
for (const palette of Object.values(
window[this.uniqueRandomID]._toolbarpalettes
)) {
let elements = Array.from(
palette.querySelectorAll(
'[wlapi_autoinjected="' + this.uniqueRandomID + '"]'
)
);
for (let element of elements) {
element.remove();
}
}
}
// Remove add-on scope, if it exists
if (window.hasOwnProperty(this.uniqueRandomID)) {
delete window[this.uniqueRandomID];
}
}
onShutdown(isAppShutdown) {
if (isAppShutdown) {
return; // the application gets unloaded anyway
}
// Unload from all still open windows
let urls = Object.keys(this.registeredWindows);
if (urls.length > 0) {
for (let window of Services.wm.getEnumerator(null)) {
//remove our entry in the add-on options menu
if (
this.pathToOptionsPage &&
(window.location.href == "chrome://messenger/content/messenger.xul" ||
window.location.href ==
"chrome://messenger/content/messenger.xhtml")
) {
if (this.getThunderbirdVersion().major < 78) {
let element_addonPrefs = window.document.getElementById(
this.menu_addonPrefs_id
);
element_addonPrefs.removeEventListener("popupshowing", this);
// Remove our entry.
let entry = window.document.getElementById(
this.menu_addonPrefs_id + "_" + this.uniqueRandomID
);
if (entry) entry.remove();
// Do we have to unhide the noPrefsElement?
if (element_addonPrefs.children.length == 1) {
let noPrefsElem = element_addonPrefs.querySelector(
'[disabled="true"]'
);
noPrefsElem.style.display = "inline";
}
} else {
// Remove event listener for addon manager view changes
let managerWindow = this.getAddonManagerFromWindow(window);
if (
managerWindow &&
managerWindow[this.uniqueRandomID] &&
managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners
) {
managerWindow.document.removeEventListener("ViewChanged", this);
managerWindow.document.removeEventListener("view-loaded", this);
managerWindow.document.removeEventListener("update", this);
let cards = this.getCards(managerWindow);
if (this.getThunderbirdVersion().major < 88) {
// Remove options menu in 78-87
for (let card of cards) {
let addonOptionsLegacyEntry = card.querySelector(
".extension-options-legacy"
);
if (addonOptionsLegacyEntry) addonOptionsLegacyEntry.remove();
}
} else {
// Remove options button in 88
for (let card of cards) {
if (card.addon.id == this.extension.id) {
let addonOptionsButton = card.querySelector(
".windowlistener-options-button"
);
if (addonOptionsButton) addonOptionsButton.remove();
break;
}
}
}
}
// Remove tabmonitor
if (window[this.uniqueRandomID].hasTabMonitor) {
this.getTabMail(window).unregisterTabMonitor(this.tabMonitor);
window[this.uniqueRandomID].hasTabMonitor = false;
}
}
}
// if it is app shutdown, it is not just an add-on deactivation
this._unloadFromWindow(window, !isAppShutdown);
}
// Stop listening for new windows.
ExtensionSupport.unregisterWindowListener(
"injectListener_" + this.uniqueRandomID
);
}
// Load registered shutdown script
let shutdownJS = {};
shutdownJS.extension = this.extension;
try {
if (this.pathToShutdownScript)
Services.scriptloader.loadSubScript(
this.pathToShutdownScript,
shutdownJS,
"UTF-8"
);
} catch (e) {
Components.utils.reportError(e);
}
// Extract all registered chrome content urls
let chromeUrls = [];
if (this.chromeData) {
for (let chromeEntry of this.chromeData) {
if (chromeEntry[0].toLowerCase().trim() == "content") {
chromeUrls.push("chrome://" + chromeEntry[1] + "/");
}
}
}
// Unload JSMs of this add-on
const rootURI = this.extension.rootURI.spec;
for (let module of Cu.loadedModules) {
if (
module.startsWith(rootURI) ||
(module.startsWith("chrome://") &&
chromeUrls.find((s) => module.startsWith(s)))
) {
this.log("Unloading: " + module);
Cu.unload(module);
}
}
// Flush all caches
Services.obs.notifyObservers(null, "startupcache-invalidate");
this.registeredWindows = {};
if (this.resourceData) {
const resProto = Cc[
"@mozilla.org/network/protocol;1?name=resource"
].getService(Ci.nsISubstitutingProtocolHandler);
for (let res of this.resourceData) {
// [ "resource", "shortname" , "path" ]
resProto.setSubstitution(res[1], null);
}
}
if (this.chromeHandle) {
this.chromeHandle.destruct();
this.chromeHandle = null;
}
}
};
quicktext-4.1/api/WindowListener/schema.json 0000664 0000000 0000000 00000005563 14106550525 0021274 0 ustar 00root root 0000000 0000000 [
{
"namespace": "WindowListener",
"functions": [
{
"name": "registerDefaultPrefs",
"type": "function",
"parameters": [
{
"name": "aPath",
"type": "string",
"description": "Relative path to the default file."
}
]
},
{
"name": "registerOptionsPage",
"type": "function",
"parameters": [
{
"name": "aPath",
"type": "string",
"description": "Path to the options page, which should be made accessible in the (legacy) Add-On Options menu."
}
]
},
{
"name": "registerChromeUrl",
"type": "function",
"description": "Register folders which should be available as chrome:// urls (as defined in the legacy chrome.manifest)",
"parameters": [
{
"name": "data",
"type": "array",
"items": {
"type": "array",
"items" : {
"type": "string"
}
},
"description": "Array of manifest url definitions (content, locale, resource)"
}
]
},
{
"name": "waitForMasterPassword",
"type": "function",
"async": true,
"parameters": []
},
{
"name": "openOptionsDialog",
"type": "function",
"parameters": [
{
"name": "windowId",
"type": "integer",
"description": "Id of the window the dialog should be opened from."
}
]
},
{
"name": "startListening",
"type": "function",
"async": true,
"parameters": []
},
{
"name": "registerWindow",
"type": "function",
"parameters": [
{
"name": "windowHref",
"type": "string",
"description": "Url of the window, which should be listen for."
},
{
"name": "jsFile",
"type": "string",
"description": "Path to the JavaScript file, which should be loaded into the window."
}
]
},
{
"name": "registerStartupScript",
"type": "function",
"parameters": [
{
"name": "aPath",
"type": "string",
"description": "Path to a JavaScript file, which should be executed on add-on startup. The script will be executed after the main application window has been sucessfully loaded."
}
]
},
{
"name": "registerShutdownScript",
"type": "function",
"parameters": [
{
"name": "aPath",
"type": "string",
"description": "Path to a JavaScript file, which should be executed on add-on shutdown."
}
]
}
]
}
]
quicktext-4.1/background.js 0000664 0000000 0000000 00000007115 14106550525 0016063 0 ustar 00root root 0000000 0000000 (async () => {
// Define default prefs.
let defaultPrefs = {
"counter": 0,
"templateFolder": "",
"defaultImport": "",
"menuCollapse": true,
"toolbar": true,
"popup": false,
"keywordKey": "Tab",
"shortcutModifier": "alt",
"shortcutTypeAdv": false,
"collapseState": ""
};
await preferences.init(defaultPrefs);
// Migrate legacy prefs using the LegacyPrefs API.
const legacyPrefBranch = "extensions.quicktext.";
const prefNames = Object.keys(defaultPrefs);
for (let prefName of prefNames) {
let legacyValue = await messenger.LegacyPrefs.getUserPref(`${legacyPrefBranch}${prefName}`);
if (legacyValue !== null) {
console.log(`Migrating legacy preference <${legacyPrefBranch}${prefName}> = <${legacyValue}>.`);
// Store the migrated value in local storage.
// Check out the MDN documentation at
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage
// or use preference.js bundled with this API
preferences.setPref(prefName, legacyValue);
// Clear the legacy value.
messenger.LegacyPrefs.clearUserPref(`${legacyPrefBranch}${prefName}`);
}
}
// Allow to set defaultImport from user_prefs
let defaultImportOverride = await messenger.LegacyPrefs.getUserPref(`${legacyPrefBranch}defaultImportOverride`);
if (defaultImportOverride !== null) {
preferences.setPref("defaultImport", defaultImportOverride);
}
// Allow to override templateFolder from user_prefs
let templateFolderOverride = await messenger.LegacyPrefs.getUserPref(`${legacyPrefBranch}templateFolderOverride`);
if (templateFolderOverride !== null) {
preferences.setPref("templateFolder", templateFolderOverride);
}
messenger.NotifyTools.onNotifyBackground.addListener(async (info) => {
switch (info.command) {
case "setPref":
preferences.setPref(info.pref, info.value);
break;
case "getPref":
return await preferences.getPref(info.pref);
break;
}
});
// load add-on via WindowListener API
messenger.WindowListener.registerChromeUrl([
["content", "quicktext", "chrome/content/"],
["resource", "quicktext", "chrome/"],
["locale", "quicktext", "de", "chrome/locale/de/"],
["locale", "quicktext", "pt-BR", "chrome/locale/pt_BR/"],
["locale", "quicktext", "en-US", "chrome/locale/en-US/"],
["locale", "quicktext", "es", "chrome/locale/es/"],
["locale", "quicktext", "fr", "chrome/locale/fr/"],
["locale", "quicktext", "hu", "chrome/locale/hu/"],
["locale", "quicktext", "ja", "chrome/locale/ja/"],
["locale", "quicktext", "ru", "chrome/locale/ru/"],
["locale", "quicktext", "sv-SE", "chrome/locale/sv-SE/"],
["locale", "quicktext", "cs", "chrome/locale/cs/"],
]);
messenger.WindowListener.registerOptionsPage("chrome://quicktext/content/addonoptions.xhtml")
messenger.WindowListener.registerWindow(
"chrome://messenger/content/messengercompose/messengercompose.xhtml",
"chrome://quicktext/content/scripts/messengercompose.js");
messenger.WindowListener.registerWindow(
"chrome://messenger/content/messenger.xhtml",
"chrome://quicktext/content/scripts/messenger.js");
browser.composeAction.onClicked.addListener(tab => { messenger.WindowListener.openOptionsDialog(tab.windowId); });
browser.browserAction.onClicked.addListener(tab => { messenger.WindowListener.openOptionsDialog(tab.windowId); });
messenger.WindowListener.startListening();
})();
quicktext-4.1/chrome/ 0000775 0000000 0000000 00000000000 14106550525 0014657 5 ustar 00root root 0000000 0000000 quicktext-4.1/chrome/content/ 0000775 0000000 0000000 00000000000 14106550525 0016331 5 ustar 00root root 0000000 0000000 quicktext-4.1/chrome/content/addonoptions.xhtml 0000664 0000000 0000000 00000000712 14106550525 0022110 0 ustar 00root root 0000000 0000000
"
: src;
} catch(e) {
Components.utils.reportError(e);
}
}
return rv;
}
,
process_text: function(aVariables)
{
if (aVariables.length != 2)
return "";
// Looks after the group and text-name and returns
// the text from it
var groupLength = this.mQuicktext.getGroupLength(false);
for (var j = 0; j < groupLength; j++)
{
if (aVariables[0] == this.mQuicktext.getGroup(j, true).name)
{
var textLength = this.mQuicktext.getTextLength(j, false);
for (var k = 0; k < textLength; k++)
{
var text = this.mQuicktext.getText(j, k, false);
if (aVariables[1] == text.name)
{
return text.text;
}
}
}
}
return "";
}
,
process_script: function(aVariables)
{
if (aVariables.length == 0)
return "";
var scriptName = aVariables.shift();
// Looks through all scripts and tries to find the
// one we look for
var scriptLength = this.mQuicktext.getScriptLength(false);
for (var i = 0; i < scriptLength; i++)
{
var script = this.mQuicktext.getScript(i, false);
if (script.name == scriptName)
{
let returnValue = "";
var referenceLineNumber = 0
try {
var error = variableNotAvailable; // provoke an error to create a reference for the other linenumber
} catch (eReference) {
referenceLineNumber = eReference.lineNumber;
}
try {
var s = Components.utils.Sandbox(this.mWindow);
s.mQuicktext = this;
s.mVariables = aVariables;
s.mWindow = this.mWindow;
returnValue = Components.utils.evalInSandbox("scriptObject = {}; scriptObject.mQuicktext = mQuicktext; scriptObject.mVariables = mVariables; scriptObject.mWindow = mWindow; scriptObject.run = function() {\n" + script.script +"\nreturn ''; }; scriptObject.run();", s);
} catch (e) {
if (this.mWindow)
{
var lines = script.script.split("\n");
// Takes the linenumber where the error where and remove
// the line that it was run on so we get the line in the script
// calculate it by using a reference error linenumber and an offset
// offset: 10 lines between "variableNotAvailable" and "evalInSandbox"
var lineNumber = e.lineNumber - referenceLineNumber - 10;
this.mWindow.alert(gQuicktext.mStringBundle.GetStringFromName("scriptError") + " " + script.name + "\n" + e.name + ": "+ e.message + "\n" + gQuicktext.mStringBundle.GetStringFromName("scriptLine") + " " + lineNumber + ": " + lines[lineNumber-1]);
}
}
return returnValue;
}
}
//if we reach this point, the user requested an non-existing script
this.mWindow.alert(gQuicktext.mStringBundle.formatStringFromName("scriptNotFound", [scriptName], 1))
return "";
}
,
process_att: function(aVariables)
{
if (this.mData['ATT'] && this.mData['ATT'].checked)
return this.mData['ATT'].data;
this.mData['ATT'] = {};
this.mData['ATT'].checked = true;
this.mData['ATT'].data = [];
// To get the attachments we look in the attachment-field
// in compose-window.
var bucket = this.mWindow.document.getElementById("attachmentBucket");
for (var index = 0; index < bucket.getRowCount(); index++)
{
var item = bucket.getItemAtIndex(index);
var attachment = item.attachment;
if (attachment)
{
var ios = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
var fileHandler = ios.getProtocolHandler("file").QueryInterface(Components.interfaces.nsIFileProtocolHandler);
try {
var file = fileHandler.getFileFromURLSpec(attachment.url);
if (file.exists())
this.mData['ATT'].data.push([attachment.name, file.fileSize]);
}
catch(e)
{
this.mData['ATT'].data.push([attachment.name]);
}
}
}
return this.mData['ATT'].data;
}
,
process_input: function(aVariables)
{
if (typeof this.mData['INPUT'] == 'undefined')
this.mData['INPUT'] = {};
if (typeof this.mData['INPUT'].data == 'undefined')
this.mData['INPUT'].data = {};
if (typeof this.mData['INPUT'].data[aVariables[0]] != 'undefined')
return this.mData['INPUT'].data;
// There are two types of input select and text.
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(Components.interfaces.nsIPromptService);
if (aVariables[1] == 'select')
{
var checkValue = {};
var value = {};
if (typeof aVariables[2] != 'undefined')
value.value = aVariables[2].split(";");
if (promptService.select(this.mWindow, gQuicktext.mStringBundle.GetStringFromName("inputTitle"), gQuicktext.mStringBundle.formatStringFromName("inputText", [aVariables[0]], 1), value.value, checkValue))
this.mData['INPUT'].data[aVariables[0]] = value.value[checkValue.value];
else
this.mData['INPUT'].data[aVariables[0]] = "";
}
else
{
var checkValue = {};
var value = {};
if (typeof aVariables[2] != 'undefined')
value.value = aVariables[2];
if (promptService.prompt(this.mWindow, gQuicktext.mStringBundle.GetStringFromName("inputTitle"), gQuicktext.mStringBundle.formatStringFromName("inputText", [aVariables[0]], 1), value, null, checkValue))
this.mData['INPUT'].data[aVariables[0]] = value.value;
else
this.mData['INPUT'].data[aVariables[0]] = "";
}
return this.mData['INPUT'].data;
}
,
process_selection: function(aVariables, aType)
{
if (aType == 0) {
// return selected text as plain text
return this.mQuicktext.mSelectionContent;
} else {
// return selected text as html
return this.mQuicktext.mSelectionContentHtml;
}
}
,
process_clipboard: function(aVariables, aType)
{
if (this.mData['CLIPBOARD'] && this.mData['CLIPBOARD'].checked)
return this.mData['CLIPBOARD'].data;
this.mData['CLIPBOARD'] = {};
this.mData['CLIPBOARD'].checked = true;
this.mData['CLIPBOARD'].data = "";
// Gets the data from the clipboard
var clip = Components.classes["@mozilla.org/widget/clipboard;1"].createInstance(Components.interfaces.nsIClipboard);
if (clip)
{
var trans = Components.classes["@mozilla.org/widget/transferable;1"].createInstance(Components.interfaces.nsITransferable);
if (trans)
{
// HTML templates: request the clipboard content as html first
var clipboardHTMLfilled = 0;
if (aType == 1)
{
trans.addDataFlavor("text/html");
clip.getData(trans, clip.kGlobalClipboard);
var clipboardHTML = {};
try {
trans.getTransferData("text/html", clipboardHTML);
if (clipboardHTML)
{
clipboardHTML = clipboardHTML.value.QueryInterface(Components.interfaces.nsISupportsString);
if (clipboardHTML)
{
this.mData['CLIPBOARD'].data = clipboardHTML.data;
clipboardHTMLfilled = 1;
}
}
}
catch (e) { Components.utils.reportError(e); }
}
// HTML templates: request clipboard content as plain text, if requesting as html failed
// Text templates: request clipboard content as plain text only
if(clipboardHTMLfilled == 0)
{
trans.addDataFlavor("text/unicode");
clip.getData(trans, clip.kGlobalClipboard);
var clipboard = {};
try {
trans.getTransferData("text/unicode", clipboard);
if (clipboard)
{
clipboard = clipboard.value.QueryInterface(Components.interfaces.nsISupportsString);
if (clipboard)
this.mData['CLIPBOARD'].data = clipboard.data;
}
}
catch (e) { Components.utils.reportError(e); }
}
}
}
return this.mData['CLIPBOARD'].data;
}
,
getcarddata_from: function(aData, aIdentity)
{
let passStandardCheck = false;
try {
let card = cardbookRepository.cardbookUtils.getCardFromEmail(aIdentity.email.toLowerCase());
if (card)
{
aData['FROM'].data['firstname'] = TrimString(card.firstname);
aData['FROM'].data['lastname'] = TrimString(card.lastname);
aData['FROM'].data['displayname'] = TrimString(card.fn);
aData['FROM'].data['nickname'] = TrimString(card.nickname);
aData['FROM'].data['fullname'] = TrimString(cardbookRepository.cardbookUtils.getName(card));
aData['FROM'].data['title'] = TrimString(card.title);
aData['FROM'].data['workphone'] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "tel.0.worktype", false));
aData['FROM'].data['faxnumber'] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "tel.0.faxtype", false));
aData['FROM'].data['cellularnumber'] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "tel.0.celltype", false));
aData['FROM'].data['custom1'] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "X-CUSTOM1", false));
aData['FROM'].data['custom2'] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "X-CUSTOM2", false));
aData['FROM'].data['custom3'] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "X-CUSTOM3", false));
aData['FROM'].data['custom4'] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "X-CUSTOM4", false));
passStandardCheck = true;
}
} catch(e) {}
if (!passStandardCheck)
{
let card = this.getCardForEmail(aIdentity.email.toLowerCase());
if (card == null && aIdentity.escapedVCard != null)
{
const manager = Cc["@mozilla.org/addressbook/msgvcardservice;1"]
.getService(Ci.nsIMsgVCardService)
card = manager.escapedVCardToAbCard(aIdentity.escapedVCard);
}
if (card != null)
{
var props = this.getPropertiesFromCard(card);
for (var p in props)
this.mData['FROM'].data[p] = props[p];
aData['FROM'].data['fullname'] = TrimString(aData['FROM'].data['firstname'] +" "+ aData['FROM'].data['lastname']);
}
}
return aData;
}
,
process_from: function(aVariables)
{
if (this.mData['FROM'] && this.mData['FROM'].checked)
return this.mData['FROM'].data;
const identity = this.mWindow.gCurrentIdentity;
this.mData['FROM'] = {};
this.mData['FROM'].checked = true;
this.mData['FROM'].data = {
'email': identity.email,
'displayname': identity.fullName,
'firstname': '',
'lastname': ''
};
this.mData = this.getcarddata_from(this.mData, identity);
return this.mData['FROM'].data;
}
,
getcarddata_to: function(aData, aIndex)
{
let passStandardCheck = false;
try {
let card = cardbookRepository.cardbookUtils.getCardFromEmail(aData['TO'].data['email'][aIndex]);
if (card)
{
aData['TO'].data['firstname'][aIndex] = TrimString(card.firstname);
aData['TO'].data['lastname'][aIndex] = TrimString(card.lastname);
aData['TO'].data['fullname'][aIndex] = TrimString(cardbookRepository.cardbookUtils.getName(card));
// others
for (let prop of [ 'displayname', 'nickname', 'title', 'workphone', 'faxnumber', 'cellularnumber', 'custom1', 'custom2', 'custom3', 'custom4' ])
{
if (typeof aData['TO'].data[prop] == 'undefined')
aData['TO'].data[prop] = []
}
aData['TO'].data['displayname'][aIndex] = TrimString(card.fn);
aData['TO'].data['nickname'][aIndex] = TrimString(card.nickname);
aData['TO'].data['title'][aIndex] = TrimString(card.title);
aData['TO'].data['workphone'][aIndex] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "tel.0.worktype", false));
aData['TO'].data['faxnumber'][aIndex] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "tel.0.faxtype", false));
aData['TO'].data['cellularnumber'][aIndex] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "tel.0.celltype", false));
aData['TO'].data['custom1'][aIndex] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "X-CUSTOM1", false));
aData['TO'].data['custom2'][aIndex] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "X-CUSTOM2", false));
aData['TO'].data['custom3'][aIndex] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "X-CUSTOM3", false));
aData['TO'].data['custom4'][aIndex] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "X-CUSTOM4", false));
passStandardCheck = true;
}
} catch(e) {}
if (!passStandardCheck)
{
// take card value, if it exists
var card = this.getCardForEmail(aData['TO'].data['email'][aIndex]);
if (card != null)
{
var props = this.getPropertiesFromCard(card);
for (var p in props)
{
if (typeof aData['TO'].data[p] == 'undefined')
aData['TO'].data[p] = []
if (props[p] != "" || typeof aData['TO'].data[p][aIndex] == 'undefined' || aData['TO'].data[p][aIndex] == "")
aData['TO'].data[p][aIndex] = TrimString(props[p]);
}
}
}
return aData;
}
,
process_to: function(aVariables)
{
if (this.mData['TO'] && this.mData['TO'].checked)
return this.mData['TO'].data;
this.mData['TO'] = {};
this.mData['TO'].checked = true;
this.mData['TO'].data = {
'email': [],
'firstname': [],
'lastname': [],
'fullname': []
};
this.mWindow.Recipients2CompFields(this.mWindow.gMsgCompose.compFields);
let emailAddresses = MailServices.headerParser.parseEncodedHeader(this.mWindow.gMsgCompose.compFields.to);
if (emailAddresses.length > 0)
{
for (var i = 0; i < emailAddresses.length; i++)
{
// TODO: Add code for getting info about all people in a mailing list
var k = this.mData['TO'].data['email'].length;
this.mData['TO'].data['email'][k] = emailAddresses[i].email.toLowerCase();
this.mData['TO'].data['fullname'][k] = TrimString(emailAddresses[i].name);
this.mData['TO'].data['firstname'][k] = "";
this.mData['TO'].data['lastname'][k] = "";
this.mData = this.getcarddata_to(this.mData, k);
let validParts = [this.mData['TO'].data['firstname'][k], this.mData['TO'].data['lastname'][k]].filter(e => e.trim() != "");
if (validParts.length == 0) {
// if no first and last name, generate them from fullname
let parts = this.mData['TO'].data['fullname'][k].replace(/,/g, ", ").split(" ").filter(e => e.trim() != "");
this.mData['TO'].data['firstname'][k] = parts.length > 1 ? TrimString(parts.splice(0, 1)) : "";
this.mData['TO'].data['lastname'][k] = TrimString(parts.join(" "));
} else {
// if we have a first and/or last name (which can only happen if read from card), generate fullname from it
this.mData['TO'].data['fullname'][k] = validParts.join(" ");
}
// swap names if wrong
if (this.mData['TO'].data['firstname'][k].endsWith(","))
{
let temp_firstname = this.mData['TO'].data['firstname'][k].replace(/,/g, "");
let temp_lastname = this.mData['TO'].data['lastname'][k];
this.mData['TO'].data['firstname'][k] = temp_lastname;
this.mData['TO'].data['lastname'][k] = temp_firstname;
// rebuild fullname
this.mData['TO'].data['fullname'][k] = [this.mData['TO'].data['firstname'][k], this.mData['TO'].data['lastname'][k]].join(" ");
}
}
}
return this.mData['TO'].data;
}
,
process_url: async function(aVariables)
{
if (aVariables.length == 0)
return "";
var url = aVariables.shift();
if (url != "")
{
var debug = false;
var method = "post";
var post = [];
if (aVariables.length > 0)
{
var variables = aVariables.shift().split(";");
for (var k = 0; k < variables.length; k++)
{
var tag = variables[k].toLowerCase();
var data = null;
switch (tag)
{
case 'to':
case 'att':
case 'orgheader':
case 'orgatt':
data = await this["process_"+ tag]();
if (typeof data != 'undefined')
{
for (var i in data)
for (var j in data[i])
post.push(tag +'['+ i +']['+ j +']='+ data[i][j]);
}
break;
case 'from':
case 'version':
case 'date':
case 'time':
data = await this["process_"+ tag]();
if (typeof data != 'undefined')
{
for (var i in data)
post.push(tag +'['+ i +']='+ data[i]);
}
break;
case 'subject':
case 'clipboard':
case 'selection':
case 'counter':
data = await this["process_"+ tag]();
if (typeof data != 'undefined')
post.push(tag +'='+ data);
break;
case 'post':
case 'get':
case 'options':
method = tag;
break;
case 'debug':
debug = true;
break;
}
}
}
var req = new XMLHttpRequest();
req.open(method, url, true);
if (method == "post") req.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
let response = "";
//Lazy async-to-sync implementation with ACK from Philipp Kewisch
//http://lists.thunderbird.net/pipermail/maildev_lists.thunderbird.net/2018-June/001205.html
let inspector = Components.classes["@mozilla.org/jsinspector;1"].createInstance(Components.interfaces.nsIJSInspector);
req.ontimeout = function () {
if (debug) response = "Quicktext timeout";
inspector.exitNestedEventLoop();
};
req.onerror = function () {
if (debug) response = "error (" + req.status + ")";
inspector.exitNestedEventLoop();
};
req.onload = function() {
if (req.status == 200) response = req.responseText;
else if (debug) response = "error (" + req.status + ")";
inspector.exitNestedEventLoop();
};
if (method == "post") req.send(post.join("&"));
else req.send();
inspector.enterNestedEventLoop(0); /* wait for async process to terminate */
return response;
}
return "";
}
,
process_version: function(aVariables)
{
if (this.mData['VERSION'] && this.mData['VERSION'].checked)
return this.mData['VERSION'].data;
this.mData['VERSION'] = {};
this.mData['VERSION'].checked = true;
this.mData['VERSION'].data = {};
this.mData['VERSION'].data['number'] = Services.appinfo.version;
this.mData['VERSION'].data['full'] = Services.appinfo.name + ' ' + Services.appinfo.version;
return this.mData['VERSION'].data;
}
,
process_counter: async function(aVariables)
{
if (this.mData['COUNTER'] && this.mData['COUNTER'].checked)
return this.mData['COUNTER'].data;
this.mData['COUNTER'] = {};
this.mData['COUNTER'].checked = true;
this.mData['COUNTER'].data = await this.mQuicktext.notifyTools.notifyBackground({command:"getPref", pref: "counter"});
this.mData['COUNTER'].data++;
await this.mQuicktext.notifyTools.notifyBackground({command:"setPref", pref: "counter", value: this.mData['COUNTER'].data});
return this.mData['COUNTER'].data;
}
,
process_subject: function(aVariables)
{
if (this.mData['SUBJECT'] && this.mData['SUBJECT'].checked)
return this.mData['SUBJECT'].data;
this.mData['SUBJECT'] = {};
this.mData['SUBJECT'].checked = true;
this.mData['SUBJECT'].data = "";
if (this.mWindow.document.getElementById('msgSubject'))
this.mData['SUBJECT'].data = this.mWindow.document.getElementById('msgSubject').value;
return this.mData['SUBJECT'].data;
}
,
process_date: function(aVariables)
{
if (this.mData['DATE'] && this.mData['DATE'].checked)
return this.mData['DATE'].data;
this.preprocess_datetime();
return this.mData['DATE'].data;
}
,
process_time: function(aVariables)
{
if (this.mData['TIME'] && this.mData['TIME'].checked)
return this.mData['TIME'].data;
this.preprocess_datetime();
return this.mData['TIME'].data;
}
,
process_orgheader: function(aVariables)
{
if (this.mData['ORGHEADER'] && this.mData['ORGHEADER'].checked)
return this.mData['ORGHEADER'].data;
this.preprocess_org();
return this.mData['ORGHEADER'].data;
}
,
process_orgatt: function(aVariables)
{
if (this.mData['ORGATT'] && this.mData['ORGATT'].checked)
return this.mData['ORGATT'].data;
this.preprocess_org();
return this.mData['ORGATT'].data;
}
,
preprocess_datetime: function()
{
this.mData['DATE'] = {};
this.mData['DATE'].checked = true;
this.mData['DATE'].data = {};
this.mData['TIME'] = {};
this.mData['TIME'].checked = true;
this.mData['TIME'].data = {};
var timeStamp = new Date();
let fields = ["DATE-long", "DATE-short", "DATE-monthname", "TIME-seconds", "TIME-noseconds"];
for (let i=0; i < fields.length; i++) {
let field = fields[i];
let fieldinfo = field.split("-");
this.mData[fieldinfo[0]].data[fieldinfo[1]] = TrimString(quicktextUtils.dateTimeFormat(field, timeStamp));
}
}
,
preprocess_org: function()
{
this.mData['ORGHEADER'] = {};
this.mData['ORGHEADER'].checked = true;
this.mData['ORGHEADER'].data = {};
this.mData['ORGATT'] = {};
this.mData['ORGATT'].checked = true;
this.mData['ORGATT'].data = {contentType:[], url:[], displayName:[], uri:[], isExternal:[]};
var msgURI = this.mWindow.gMsgCompose.originalMsgURI;
if (!msgURI || msgURI == "")
return;
var messenger = Components.classes["@mozilla.org/messenger;1"].createInstance(Components.interfaces.nsIMessenger);
var mms = messenger.messageServiceFromURI(msgURI).QueryInterface(Components.interfaces.nsIMsgMessageService);
//Lazy async-to-sync implementation with ACK from Philipp Kewisch
//http://lists.thunderbird.net/pipermail/maildev_lists.thunderbird.net/2018-June/001205.html
let inspector = Components.classes["@mozilla.org/jsinspector;1"].createInstance(Components.interfaces.nsIJSInspector);
let listener = streamListener(inspector);
mms.streamMessage(msgURI, listener, null, null, true, "filter");
//lazy async, wait for listener
inspector.enterNestedEventLoop(0); /* wait for async process to terminate */
// Store all headers in the mData-variable
for (var i = 0; i < listener.mHeaders.length; i++)
{
var name = listener.mHeaders[i].name;
if (typeof this.mData['ORGHEADER'].data[name] == 'undefined')
this.mData['ORGHEADER'].data[name] = [];
this.mData['ORGHEADER'].data[name].push(listener.mHeaders[i].value);
}
// Store all attachments in the mData-variable
for (var i = 0; i < listener.mAttachments.length; i++)
{
var attachment = listener.mAttachments[i];
for (var fields in attachment)
this.mData['ORGATT'].data[fields][i] = attachment[fields];
}
}
,
escapeRegExp: function(aStr)
{
return aStr.replace(/([\^\$\_\.\\\[\]\(\)\|\+\?])/g, "\\$1");
}
,
replaceText: function(tag, value, text)
{
var replaceRegExp;
if (value != "")
replaceRegExp = new RegExp(this.escapeRegExp(tag), 'g');
else
replaceRegExp = new RegExp("( )?"+ this.escapeRegExp(tag), 'g');
return text.replace(replaceRegExp, value);
}
,
niceFileSize: function(size)
{
var unit = ["B", "kB", "MB", "GB", "TB"];
var i = 0;
while (size > 1024)
{
i++;
size = size / 1024;
}
return (Math.round(size * 100) / 100) + " " + unit[i];
}
,
getCardForEmail: function(aAddress) {
let directories = MailServices.ab.directories;
for (let addrbook of directories)
{
let card = addrbook.cardForEmailAddress(aAddress);
if (card) {
return card;
}
}
return null;
}
,
getPropertiesFromCard: function(card)
{
var retval = {}
var props = card.properties;
for (let prop of props)
{
retval[prop.name.toLowerCase()] = prop.value;
}
return retval;
}
,
removeBadHTML: function (aStr)
{
// Remove the head-tag
aStr = aStr.replace(/