116 | function parseHTMLFragment(htmlString) {
117 | const parser = new DOMParser();
118 | const doc = parser.parseFromString(htmlString, "text/html");
119 |
120 | // Spread childNodes to an array to avoid live list mutation issues
121 | const nodes = [...doc.body.childNodes];
122 | const fragment = document.createDocumentFragment();
123 | // appendChild moves nodes from doc.body to fragment (not cloned)
124 | nodes.forEach((node) => fragment.appendChild(node));
125 | return fragment;
126 | }
127 |
128 | // copy a html structure into [multiple] elements
129 | // eslint-disable-next-line no-unused-vars
130 | function updateWithSafeHtml(selector, htmlString) {
131 | const elements = document.querySelectorAll(selector);
132 | for (const el of elements) {
133 | el.textContent = "";
134 | el.appendChild(parseHTMLFragment(htmlString));
135 | }
136 | }
137 |
138 | // eslint-disable-next-line no-unused-vars
139 | async function resizeWindow() { // was in updateActions()
140 | // resize to contents if necessary...
141 | try {
142 | const win = await browser.windows.getCurrent();
143 | let wrapper = document.getElementById("messageCanvas"),
144 | r = wrapper.getBoundingClientRect(),
145 | newHeight = Math.round(r.height) + 80,
146 | maxHeight = window.screen.height;
147 |
148 | let { os } = await messenger.runtime.getPlatformInfo(); // mac / win / linux
149 | wrapper.setAttribute("os", os);
150 |
151 | if (newHeight > maxHeight) {
152 | newHeight = maxHeight - 15;
153 | }
154 | browser.windows.update(win.id, { height: newHeight });
155 | } catch(e) {
156 | console.error("Failed to resize window:", e);
157 | return;
158 | }
159 | }
--------------------------------------------------------------------------------
/html/fq-settings.js:
--------------------------------------------------------------------------------
1 | /*
2 | globals
3 | i18n,
4 | */
5 |
6 |
7 | // add event listeners for tabs
8 | const activateTab = (event) => {
9 | const tabSheets = document.querySelectorAll(".tabcontent-container section"),
10 | tabs = document.querySelectorAll("#FiltaQuilla-Options-Tabbox button");
11 | let btn = event.target;
12 | Array.from(tabSheets).forEach(tabSheet => {
13 | tabSheet.classList.remove("active");
14 | });
15 | Array.from(tabs).forEach(button => {
16 | button.classList.remove("active");
17 | button.parentElement.removeAttribute("aria-selected");
18 | });
19 |
20 |
21 | const { target: { value: activeTabSheetId = "" } } = event;
22 | if (activeTabSheetId) {
23 | document.getElementById(activeTabSheetId).classList.add("active");
24 | btn.classList.add("active");
25 | btn.parentElement.setAttribute("aria-selected", true); // li
26 | // store last selected tab
27 | browser.LegacyPrefs.setPref(
28 | "extensions.filtaquilla.lastSelectedOptionsTab",
29 | btn.value
30 | );
31 | }
32 | }
33 |
34 | const initPrefs = async () => {
35 | // checkboxes
36 | const checkboxes = document.querySelectorAll("input[type=checkbox][data-pref-name]");
37 | for (const el of checkboxes) {
38 | const prefName = el.getAttribute("data-pref-name");
39 | const value = await messenger.LegacyPrefs.getPref(prefName);
40 | el.checked = !!value;
41 |
42 | el.addEventListener("change", () => {
43 | messenger.LegacyPrefs.setPref(prefName, el.checked);
44 | });
45 | }
46 |
47 | // text / number inputs
48 | const inputs = document.querySelectorAll(
49 | "input[type=text][data-pref-name], input[type=number][data-pref-name]"
50 | );
51 | for (const el of inputs) {
52 | const prefName = el.getAttribute("data-pref-name");
53 | const value = await messenger.LegacyPrefs.getPref(prefName);
54 | el.value = value ?? "";
55 |
56 | el.addEventListener("input", () => {
57 | messenger.LegacyPrefs.setPref(prefName, el.type === "number" ? Number(el.value) : el.value);
58 | });
59 | }
60 | document.getElementById("changeLog").textContent = messenger.i18n.getMessage(
61 | "message.btn.changeLog", ""
62 | );
63 | };
64 |
65 | /**** FLOATING TOOLTIPS ===> **** */
66 | function toggleTooltip(button) {
67 | const row = button.closest(".option-horizontal");
68 | if (!row) {
69 | return;
70 | }
71 |
72 | const tooltip = row.querySelector(".tooltip-bubble");
73 | if (!tooltip) {
74 | return;
75 | }
76 |
77 | // Hide all other tooltips first
78 | document.querySelectorAll(".tooltip-bubble").forEach((t) => {
79 | if (t !== tooltip) {
80 | t.hidden = true;
81 | }
82 | });
83 | document.querySelectorAll(".tooltipBtn").forEach((t) => {
84 | t.removeAttribute("tooltipshown");
85 | });
86 |
87 | // Toggle this one
88 | tooltip.hidden = !tooltip.hidden;
89 | if (tooltip.hidden) {
90 | button.removeAttribute("tooltipshown");
91 | } else {
92 | button.setAttribute("tooltipshown", true);
93 | }
94 | }
95 |
96 |
97 | const initEventListeners = async () => {
98 | for (let button of document.querySelectorAll("#FiltaQuilla-Options-Tabbox button")) {
99 | button.addEventListener("click", activateTab);
100 | }
101 |
102 | // Tooltip buttons
103 | for (let btn of document.querySelectorAll(".tooltipBtn")) {
104 | btn.addEventListener("click", () => {
105 | toggleTooltip(btn);
106 | });
107 | }
108 |
109 | document.addEventListener("click", (ev) => {
110 | const btn = ev.target.closest(".helpLink") || ev.target.closest(".tooltipBtn");
111 | if (!btn) {
112 | return;
113 | }
114 | if (btn.classList.contains("helpLink")) {
115 | const topic = btn.getAttribute("helptopic");
116 | if (!topic) {
117 | return;
118 | }
119 | FiltaQuilla.Util.openHelpTab(topic);
120 | }
121 | });
122 |
123 | addConfigEvent(document.getElementById("debug-options"), "extensions.filtaquilla.debug");
124 | document
125 | .getElementById("fq-options-header-version")
126 | .addEventListener("click", (event) => onVersionClick(event.target));
127 |
128 | const changeLog = document.getElementById("changeLog");
129 | changeLog.addEventListener("click", (_event) => {
130 | messenger.runtime.sendMessage({
131 | command: "showMessage",
132 | msgIds: "whats-new-list",
133 | mode: "standard",
134 | features: ["ok"],
135 | });
136 | window.close();
137 | });
138 | }
139 |
140 | async function dispatchAboutConfig(filter, readOnly, updateUI = false) {
141 | // we put the notification listener into quickfolders-tablistener.js - should only happen in ONE main window!
142 | // el - cannot be cloned! let's throw it away and get target of the event
143 | messenger.runtime.sendMessage({
144 | command: "showAboutConfig",
145 | filter: filter,
146 | readOnly: readOnly,
147 | updateUI: updateUI,
148 | });
149 | }
150 |
151 | async function onVersionClick(el) {
152 | let pureVersion = await FiltaQuilla.Util.getVersionSanitized(el.textContent),
153 | versionPage = "https://quickfilters.quickfolders.org/fq-versions.html#" + pureVersion;
154 | FiltaQuilla.Util.openLinkInTab(versionPage);
155 | }
156 |
157 | function addConfigEvent(el, filterConfig) {
158 | // add right-click event to containing label
159 | if (!el) {
160 | return;
161 | }
162 | // Use closest to find the nearest .configSettings button, or fallback to parent if not found
163 | let eventNode = el.closest(".hasConfigEvent").querySelector(".configSettings");
164 | let eventType;
165 | if (eventNode) {
166 | eventType = "click";
167 | } else {
168 | eventNode = el.parentNode;
169 | eventType = "contextmenu";
170 | }
171 | eventNode.addEventListener(eventType, async (event) => {
172 | event.preventDefault();
173 | event.stopPropagation();
174 | await dispatchAboutConfig(filterConfig, true, true);
175 | // if (null!=retVal) return retVal;
176 | });
177 | }
178 |
179 |
180 | const startup = async () => {
181 | i18n.updateDocument();
182 | await initEventListeners();
183 | await initPrefs();
184 |
185 | const verPanel = document.getElementById("fq-options-header-version");
186 | const manifest = browser.runtime.getManifest();
187 | verPanel.textContent = manifest.version;
188 |
189 | }
190 | startup();
--------------------------------------------------------------------------------
/skin/filtaquilla-prefs.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --version-background: linear-gradient(to bottom, #0380bf 0%,#006eb7 100%) !important;
3 | }
4 |
5 | /* styling for filtaquilla preferences dialog */
6 |
7 | .buttonLink {
8 | cursor: pointer !important;
9 | padding: 4px 16px;
10 | margin: 4px auto 6px;
11 | border: 1px solid rgba(120,120,120,0.5);
12 | box-shadow: 4px 4px 3px rgba(80,80,80,0.3);
13 | }
14 |
15 | #actionsGrid checkbox,
16 | #conditionsGrid checkbox {
17 | white-space: nowrap;
18 | }
19 |
20 | #supportLink {
21 | background: linear-gradient(to bottom, #67b6ce 0%,#316c8c 31%,#066dab 100%);
22 | color: #FFF;
23 | }
24 |
25 | #supportLink:hover {
26 | background: linear-gradient(to bottom, #6ecfec 0%,#1e98da 31%,#0492ea 100%);
27 | }
28 |
29 | #quickFiltersLink {
30 | background: linear-gradient(to bottom, #cb60b3 0%,#c146a1 50%,#a80077 51%,#db36a4 100%);
31 | color: #FFF;
32 | }
33 |
34 | #quickFiltersLink:hover {
35 | background: linear-gradient(to bottom, #f85032 0%,#f16f5c 50%,#f6290c 51%,#f02f17 71%,#e73827 100%);
36 | }
37 |
38 | #licenseLink {
39 | background: linear-gradient(to bottom, #f78a4f 0%,#f05a04 31%,#b84300 61%,#f7782f 100%);
40 | color: #FFF;
41 | }
42 |
43 | #licenseLink:hover {
44 | background: linear-gradient(to bottom, #fdd8b3 0%,#fea546 31%,#f47e00 61%,#fdc589 100%);
45 | }
46 |
47 | div.lineSpacer {
48 | display: block;
49 | border-bottom: 1px solid gray;
50 | }
51 |
52 | .customLogo {
53 | margin-left: 5px;
54 | width: 32px;
55 | height: 32px;
56 | }
57 |
58 | #filtaquilla_img {
59 | /* just a hack as I don't appear to be able to use img url for a chrome address? */
60 | background-image:url("resource://filtaquilla-skin/filtaquilla-32.png");
61 | background-repeat: no-repeat;
62 | }
63 |
64 | #quickfilters_img {
65 | /* just a hack as I don't appear to be able to use img url for a chrome address? */
66 | background-image:url("resource://filtaquilla-skin/quickfilters.png");
67 | background-repeat: no-repeat;
68 | }
69 |
70 | #community_img {
71 | background-image:url("resource://filtaquilla-skin/community.png");
72 | background-repeat: no-repeat;
73 | }
74 |
75 | .helpLink,
76 | .helpLinkDisabled {
77 | appearance: none !important;
78 | background-repeat: no-repeat;
79 | cursor: pointer;
80 | margin-top: 3px;
81 | width: 16px !important;
82 | min-width: 22px;
83 | }
84 |
85 | .helpLink {
86 | background-image: url("resource://filtaquilla-skin/fugue-question.png");
87 | }
88 | .helpLinkDisabled {
89 | background-image: url("resource://filtaquilla-skin/fugue-question-disabled.png");
90 | }
91 |
92 |
93 | .helpLink .tooltip {
94 | position: fixed;
95 | font-size: 14px;
96 | line-height: 20px;
97 | padding: 5px;
98 | background: white;
99 | border: 1px solid #ccc;
100 | visibility: hidden;
101 | opacity: 0;
102 | box-shadow: -2px 2px 5px rgba(0, 0, 0, 0.2);
103 | transition: opacity 0.3s, visibility 0s;
104 | right: 20px !important; /* avoid cut off on the right */
105 | }
106 |
107 | .helpLink:hover .tooltip {
108 | visibility: visible;
109 | opacity: 1;
110 | }
111 |
112 | hbox.title {
113 | display: inline-flex; /* make child elements flex */
114 | font-size: 20px;
115 | font-weight: 500;
116 | }
117 |
118 | hbox.title spacer {
119 | display: inline-block;
120 | flex-grow: 5;
121 | }
122 |
123 | #fq-options-header-version {
124 | background-color: rgba(80,80,80,0.13);
125 | padding-left: 0.25em;
126 | padding-right: 0.25em;
127 | }
128 |
129 | #fq-options-header-version:hover {
130 | color: white;
131 | background-color: rgba(80,80,80,0.5);
132 | }
133 |
134 | .linkContainer {
135 | margin: 0 20px 1em 20px !important;
136 | max-width: 660px;
137 | }
138 |
139 | .linkBox {
140 | border: 1px solid rgb(230,230,230);
141 | padding-top: 0.75em;
142 | white-space: normal !important;
143 | }
144 |
145 | .linkDescriptionBox vbox {
146 | vertical-align: top;
147 | display: table-cell;
148 | }
149 |
150 | .linkDescriptionBox img {
151 | position: relative;
152 | top: auto;
153 | }
154 |
155 | .linkBox hbox {
156 | max-width: 55em;
157 | }
158 |
159 | .linkDescriptionBox {
160 | display: table;
161 | }
162 |
163 | .linkDescription {
164 | display: table-cell;
165 | margin: 0 5px;
166 | max-width: 50em !important;
167 | padding: 4px;
168 | padding-bottom: 0.7em;
169 | text-align: left;
170 | width: 50em !important;
171 | white-space: wrap;
172 | }
173 |
174 | .linkDescription * {
175 | max-width: 50em !important;
176 | }
177 |
178 | #actionCols column {
179 | min-width: 175px;
180 | }
181 |
182 | .icontabs tab {
183 | padding: 5px 8px;
184 | margin-top: 5px;
185 | }
186 |
187 | .icontabs tab label {
188 | margin-left:5px !important;
189 | }
190 |
191 | #filterActionsTab {
192 | list-style-image: url("resource://filtaquilla-skin/fugue-arrow-curve.png") !important;
193 | }
194 |
195 | #conditionsTab {
196 | list-style-image: url("resource://filtaquilla-skin/script.png") !important;
197 | }
198 |
199 | #supportTab {
200 | list-style-image: url("resource://filtaquilla-skin/fugue-question.png") !important;
201 | }
202 |
203 |
204 |
205 | /* grid replacement */
206 | .grid-container {
207 | align-content: start;
208 | display: grid;
209 | grid-row-gap: 1px;
210 | grid-column-gap: 5px;
211 | padding-top: 12px;
212 | }
213 |
214 | #actionsGrid,
215 | #conditionsGrid {
216 | grid-template-columns: auto auto auto auto; /* widths of columns */
217 | }
218 |
219 | .showSplash, .showSplash:focus {
220 | color: white !important;
221 | background-color: #006EB7 !important; /* blue */
222 | background-image: var(--version-background) !important;
223 | }
224 | .showSplash label, .showSplash:focus label {
225 | color: white;
226 | }
227 |
228 | .showSplash:hover, .showSplash:focus:hover {
229 | color: black !important;
230 | background-color: #ffd65e !important; /* yellow */
231 | background-image: linear-gradient(to bottom, #fff65e 0%,#febf04 100%) !important;
232 | }
233 |
234 | .showSplash:hover *, .showSplash:focus:hover * {
235 | color: rgb(60,0,0) !important;
236 | }
237 | .buttonLinks {
238 | margin-block: 0.5em;
239 | }
240 |
241 |
242 | .buttonLinks button {
243 | cursor: pointer;
244 | padding-block: 0.5em;
245 | }
246 |
247 |
248 | .buttonLinks button.text-link {
249 | min-width: 25em;
250 | padding-inline: 1.5em !important;
251 | }
252 |
253 | button.text-link {
254 | appearance: none !important;
255 | border: 1px solid rgb(100,120,120);
256 | color: #ffffff !important;
257 | margin-top: 0.3em !important;
258 | margin-bottom: 0.3em !important;
259 | max-width: 36em;
260 | padding: 1px 1px !important;
261 | text-decoration: none !important;
262 | text-shadow:#BBBBBB 0px 0px 2px;
263 | }
264 |
--------------------------------------------------------------------------------
/content/api/FiltaQuilla/implementation.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 |
4 | /*
5 | const christophers_code = async () => {
6 | let attPartName = att.url.match(/part=([.0-9]+)&?/)[1];
7 | let attFile = await getAttachmentFile(aMsgHdr, attPartName);
8 | let fileData = await fileToUint8Array(attFile);
9 | await IOUtils.write(attDirContainerClone.path, fileData);
10 | };
11 | */
12 |
13 | /*
14 | globals
15 | ExtensionCommon,
16 | */
17 |
18 | // Using a closure to not leak anything but the API to the outside world.
19 | (function (exports) {
20 | const lazy = {};
21 | var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
22 | // const thunderbirdVersion = parseInt(Services.appinfo.version.split(".")[0], 10);
23 | const getters = ["FileReader", "IOUtils"];
24 | XPCOMUtils.defineLazyGlobalGetters(lazy, getters);
25 |
26 | function sanitizeName(aName, includesExtension = false) {
27 | const win = Services.wm.getMostRecentWindow("mail:3pane");
28 | return win.FiltaQuilla.sanitizeName(aName, includesExtension);
29 | }
30 |
31 | var FiltaQuilla = class extends ExtensionCommon.ExtensionAPI {
32 | getAPI(_context) {
33 | return {
34 | FiltaQuilla: {
35 | async saveFile(file, path, fileName) {
36 | const Cc = Components.classes;
37 | const Ci = Components.interfaces;
38 | const newName = sanitizeName(fileName || file.name, true);
39 | const win = Services.wm.getMostRecentWindow("mail:3pane");
40 | const util = win.FiltaQuilla.Util;
41 | util.logDebug(`new file name would be: ${newName}`, util);
42 |
43 | const pathFile = await lazy.IOUtils.createUniqueFile(path, newName, 0o600);
44 | const saveFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
45 | saveFile.initWithPath(pathFile);
46 | util.logDebugOptional("attachments", `Saving to path: ${pathFile}...`);
47 |
48 | try {
49 | // Check if FileReader is defined as an object (WebExtension context)
50 | // ============================== RELEASE CODE ======================
51 | const bytes = await new Promise(function (resolve, reject) {
52 | const reader = new lazy.FileReader();
53 | reader.onloadend = function () {
54 | resolve(new Uint8Array(reader.result));
55 | };
56 | reader.onerror = function () {
57 | reject(new Error("FileReader error"));
58 | };
59 | reader.readAsArrayBuffer(file);
60 | });
61 | await lazy.IOUtils.write(pathFile, bytes);
62 | return true;
63 | } catch (ex) {
64 | console.error(ex, "FiltaQuilla.saveFile()", path);
65 | return false;
66 | }
67 | },
68 | showAboutConfig: function (filter) {
69 | const name = "Preferences:ConfigManager",
70 | mediator = Services.wm,
71 | uri = "about:config";
72 |
73 | let w = mediator.getMostRecentWindow(name),
74 | win = mediator.getMostRecentWindow("mail:3pane");
75 |
76 | if (!w) {
77 | let watcher = Services.ww;
78 | w = watcher.openWindow(
79 | win,
80 | uri,
81 | name,
82 | "chrome,resizable,centerscreen,width=800px,height=380px",
83 | null
84 | );
85 | }
86 | w.focus();
87 | w.addEventListener("load", function () {
88 | let id = "about-config-search",
89 | flt = w.document.getElementById(id);
90 | if (flt) {
91 | flt.value = filter;
92 | // make filter box readonly to prevent damage!
93 | flt.setAttribute("readonly", true);
94 | if (w.self.FilterPrefs) {
95 | w.self.FilterPrefs();
96 | }
97 | }
98 | });
99 | },
100 | detachAttachments: async function (messageId, savedAttachments) {
101 | // probably obsolete. Hence no schema entry.
102 | console.log(`detachAttachments called for messageId: ${messageId}`);
103 | const win = Services.wm.getMostRecentWindow("mail:3pane");
104 | const extension = win.FiltaQuilla.Util.extension;
105 | const msgHdr = extension.messageManager.get(messageId);
106 | if (!msgHdr) {
107 | console.warn(`messageManager could not retrieve valid message header from id ${messageId}`);
108 | return false;
109 | }
110 | // 1. Open message via msgDatabase
111 | const msgDB = msgHdr.folder.msgDatabase;
112 | if (!msgDB) {
113 | console.warn(`couldn't retrieve msgDabase for ${msgHdr?.folder?.URI}`);
114 | return false;
115 | }
116 | /*
117 | var { AttachmentInfo } = ChromeUtils.importESModule(
118 | "resource:///modules/AttachmentInfo.sys.mjs"
119 | );
120 | */
121 |
122 | for (let at of savedAttachments) {
123 | /*
124 | const newAttachment = new AttachmentInfo({
125 | contentType: at.contentType,
126 | url: "test",
127 | name: at.partName,
128 | uri,
129 | isExternalAttachment,
130 | message: msgHdr,
131 | updateAttachmentsDisplayFn: null,
132 | });
133 | */
134 | console.log(`restoring ${at} from ${at?.path}...`);
135 | // TODO: implement actual logic
136 | // 2. Find the stub corresponding to at.partName
137 | let part = msgDB.getAttachmentInfo(at.partName);
138 | if (!part) {
139 | console.warn(`couldn't find part ${at.partName}, skipping`);
140 | continue;
141 | }
142 | // 3. Update headers or metadata to point to at.path
143 | // 4. Optionally refresh UI or trigger any required events
144 | }
145 | return true;
146 | },
147 | },
148 | };
149 | }
150 |
151 | onShutdown(isAppShutdown) {
152 | if (isAppShutdown) {
153 | return; // the application gets unloaded anyway
154 | }
155 |
156 | // Flush all caches.
157 | Services.obs.notifyObservers(null, "startupcache-invalidate");
158 | }
159 | };
160 | exports.FiltaQuilla = FiltaQuilla;
161 | })(this);
162 |
--------------------------------------------------------------------------------
/content/api/NotifyTools/README.md:
--------------------------------------------------------------------------------
1 | # Objective
2 |
3 | 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).
4 |
5 | 
6 |
7 | They allow to work on add-on uprades in smaller steps, as single calls (like `window.openDialog()`)
8 | in the middle of legacy code can be replaced by WebExtension calls, by stepping out of the Experiment
9 | and back in when the task has been finished.
10 |
11 | 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).
12 |
13 | # Example
14 |
15 | This repository includes the [NotifyToolsExample Add-On](https://github.com/thundernest/addon-developer-support/raw/master/auxiliary-apis/NotifyTools/notifyToolsExample.zip), showcasing how the NotifyTools can be used.
16 |
17 | # Usage
18 |
19 | 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:
20 |
21 | ```
22 | "experiment_apis": {
23 | "NotifyTools": {
24 | "schema": "api/NotifyTools/schema.json",
25 | "parent": {
26 | "scopes": ["addon_parent"],
27 | "paths": [["NotifyTools"]],
28 | "script": "api/NotifyTools/implementation.js",
29 | "events": ["startup"]
30 | }
31 | }
32 | },
33 | ```
34 |
35 | 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.
36 |
37 | **Note:** You need to adjust the `notifyTools.js` script and add your add-on ID at the top.
38 |
39 | ## Receiving notifications from Experiment scripts
40 |
41 | Add a listener for the `onNotifyBackground` event in your WebExtension's background page:
42 |
43 | ```
44 | messenger.NotifyTools.onNotifyBackground.addListener(async (info) => {
45 | switch (info.command) {
46 | case "doSomething":
47 | //do something
48 | let rv = await doSomething();
49 | return rv;
50 | break;
51 | }
52 | });
53 | ```
54 |
55 | The `onNotifyBackground` event will receive and respond to notifications send from your Experiment scripts:
56 |
57 | ```
58 | notifyTools.notifyBackground({command: "doSomething"}).then((data) => {
59 | console.log(data);
60 | });
61 | ```
62 |
63 | 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()`. If you are injecting the script into a global Thunderbird window object, make sure to wrap it in your custom namespace, to prevent clashes with other add-ons.
64 |
65 | **Note**: If multiple `onNotifyBackground` listeners are registered in the WebExtension's background page and more than one is returning data, the value
66 | from the first one is returned to the Experiment. This may lead to inconsistent behavior, so make sure that for each
67 | request only one listener is returning data.
68 |
69 |
70 | ## Sending notifications to Experiments scripts
71 |
72 | Use the `notifyExperiment()` method to send a notification from the WebExtension's background page to Experiment scripts:
73 |
74 | ```
75 | messenger.NotifyTools.notifyExperiment({command: "doSomething"}).then((data) => {
76 | console.log(data)
77 | });
78 | ```
79 |
80 | 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:
81 |
82 | ### addListener(callback);
83 |
84 | Adds a callback function, which is called when a notification from the WebExtension's background page has been received. The `addListener()` function returns an `id` which can be used to remove the listener again.
85 |
86 | Example:
87 |
88 | ```
89 | function doSomething(data) {
90 | console.log(data);
91 | return true;
92 | }
93 | let id = notifyTools.addListener(doSomething);
94 | ```
95 |
96 | **Note**: NotifyTools currently is not 100% compatible with the behavior of
97 | runtime.sendMessage. While runtime messaging is ignoring non-Promise return
98 | values, NotifyTools only ignores `null`.
99 |
100 | Why does this matter? Consider the following three listeners:
101 |
102 | ```
103 | async function dominant_listener(data) {
104 | if (data.type == "A") {
105 | return { msg: "I should answer only type A" };
106 | }
107 | }
108 |
109 | function silent_listener(data) {
110 | if (data.type == "B") {
111 | return { msg: "I should answer only type B" };
112 | }
113 | }
114 |
115 | function selective_listener(data) {
116 | if (data.type == "C") {
117 | return Promise.resolve({ msg: "I should answer only type C" });
118 | }
119 | }
120 | ```
121 |
122 | When all 3 listeners are registered for the runtime.onMessage event,
123 | the dominant listener will always respond, even for `data.type != "A"` requests,
124 | because it is always returning a Promise (it is an async function). The return
125 | value of the silent listener is ignored, and the selective listener returns a
126 | value just for `data.type == "C"`. But since the dominant listener also returns
127 | `null` for these requests, the actual return value depends on which listener is faster
128 | and/or was registered first.
129 |
130 | All notifyTools listener however ignore only `null` return values (so `null` can
131 | actually never be returned). The above dominant listener will only respond to
132 | `type == "A"` requests, the silent listener will only respond to `type == "B"`
133 | requests and the selective listener will respond only to `type == "C"` requests.
134 |
135 | ### removeListener(id)
136 |
137 | Removes the listener with the given `id`.
138 |
139 | Example:
140 |
141 | ```
142 | notifyTools.removeListener(id);
143 | ```
144 |
145 | ### removeAllListeners()
146 |
147 | You must remove all added listeners when your add-on is disabled/reloaded. Instead of calling `removeListener()` for each added listener, you may call `removeAllListeners()`.
148 |
149 | ### setAddOnId(add-on-id)
150 |
151 | The `notifyTools.js` script needs to know the ID of your add-on to be able to listen for messages. You may either define the ID directly in the first line of the script, or set it using `setAddOnId()`.
--------------------------------------------------------------------------------
/changes.txt:
--------------------------------------------------------------------------------
1 | ** Changes list **
2 |
3 | 5.0 - published 05/04/2025
4 |
5 | **Improvements**
6 | * Made compatible with Thunderbird 137.\*. Minimum version going forward will now be **Thunderbird 128**.
7 | * The helper function `saveAllAttachments()` was removed and had to be reimplemented going through web extension layer. The save attachments action is now set to beind asynchronous. This might potentially improve overall performance in Thunderbird. \[issue #319\].
8 | * Save Messages: Made file operations asynchronous. This should avoid significant slowdowns when Thunderbird starts and tries to execute filters that potentially save many files or attachments. \[issue #335\]
9 | * Header Regex match: Support using anchor tokens `^` and `$` for address lists \[issue #329\].
10 | * Allow automatic running of filters outside of Inbox (IMAP only) \[issue #318\].
11 | * Thunderbird 136 retires ChromeUtils.import - replace with importESModule. Also converted all jsm modules to ESM modules. \[issue #331\].
12 |
13 | **Miscellaneus**
14 | * Replace deprecated `nsILocalFile` with `nsIFile`
15 |
16 |
17 | 5.0.1 - published 08/04/2025
18 |
19 | **Improvements**
20 | * Made compatible with Thunderbird 138.\*. Minimum version going forward will now be **Thunderbird 128**.
21 | * The helper function saveAllAttachments() was removed and had to be reimplemented going through web extension layer. The save attachments action is now set to beind asynchronous. This might potentially improve overall performance in Thunderbird. \[issue #319\].
22 | * Save Messages: Made file operations asynchronous. This should avoid significant slowdowns when Thunderbird starts and tries to execute filters that potentially save many files or attachments. \[issue #335\]
23 | * Header Regex match: Support using anchor tokens ^ and $ for address lists \[issue #329\].
24 | * Thunderbird 136 retires ChromeUtils.import - replace with importESModule. Also converted all jsm modules to ESM modules. \[issue #331\].
25 |
26 | **Miscellaneus**
27 | * Replace deprecated nsILocalFile with nsIFile
28 |
29 |
30 | 5.1 - published 12/04/2025
31 |
32 | **Improvements**
33 | * Made compatible with Thunderbird 138.\*. Minimum version going forward will now be **Thunderbird 128**.
34 | * The helper function saveAllAttachments() was removed and had to be reimplemented going through web extension layer. The save attachments action is now set to beind asynchronous. This might potentially improve overall performance in Thunderbird. \[issue #319\].
35 |
36 | **Miscellaneus**
37 | * Remove declaration of Services \[issue #337\]
38 |
39 | **Bug Fixes**
40 | * Error when using Javascript for a Saved Search criteria (Tb 137) [issue #338]. The existing xhtml window for editing javascript stopped working in Thunderbird 136, therefore I rewrote the feature using a standard HTML window and more modern back-end code.
41 | Also, in later versions of Thunderbird 128, the use of eval() triggers a CSP exception and thus does not work at all anymore. I reimplemented the scripting using the more restricted (and safer) evalInSandbox, which only gives a limited, controlled scope to the environment that is accessible from the script.
42 |
43 |
44 |
45 | 5.2 - published 13/05/2025
46 |
47 | **Improvements**
48 | * Made compatible with Thunderbird 139.\*. Minimum version going forward will now be **Thunderbird 128**.
49 | * Make Save Attachments Asynchronous, Future Proof for Thunderbrid 128, 140 and release channel [issue #347]. It is highly recommended to run filter after junk detection as this will not potentially lock up the user interface when Thunderbird starts up.
50 | * Fixed: save message as file (with custom extension) - does not work anymore [issue #343]
51 |
52 | **TO DO**
53 | * Work in progress: Allow automatic running of filters outside of Inbox (IMAP only) \[issue #318\].
54 | As adding the checkbox in folder properties didn't meet policy restrictions, we are planning to add a web extension compatible interface for this at a later stage, possible through the folder tree context menu.
55 | * Test attachRegEx_match and see if it needs updates for Tb128 / Release
56 |
57 |
58 |
59 | 5.3. - Published 25/06/2025
60 |
61 | **Improvements**
62 | * Made compatible with Thunderbird 140.\*. Minimum version going forward will now be **Thunderbird 128**.
63 | * Improvement in asynchronous Save Attachments, leading to slow down of Thunderbird when filtering POP3 mail (no copy listener) [issue #349]. We are now allowed to use `nsIThreadManager.processNextEvent()` in order to give cycles back to the system while the attachments are processed. Please restart Thunderbird to force changes to come into effect.
64 | * Play sound improvements: Fixed open sound file button, extracting the supplied sounds to the default folder `profile/extensions/filtaquilla` and added a play sound button to filter editor. [issue #350]
65 |
66 | **Miscellaneus**
67 | * Refactored internal action logic for better maintainability and consistency.
68 | * Rewrote `saveMessageAsFile` to support concurrency and cleaner path handling.
69 |
70 |
71 | 5.3.1 - published 24/07/2025
72 |
73 | **Improvements**
74 | * Made compatible with Thunderbird 141.\*.
75 |
76 | **Miscellaneus**
77 | * Thunderbird 141 removed `nsIMsgFolder.prettyName`
78 |
79 |
80 | 5.4 - Published 29/09/2025
81 |
82 | **Improvements**
83 | * Made compatible with Thunderbird 144.*.
84 | * Rewrote the Tonequilla portion (play sound) to use current window as a parameter or the last 3pane window. [issue #258]
85 |
86 |
87 | 5.5 - published 13/10/2025
88 |
89 | **Improvements**
90 | * Made compatible with Thunderbird 145.*.
91 | * Converted settings dialog to html \[issue #366\]
92 | * Added localisations for French, Japanese, Italian and Spanish users.
93 | * Added a toolbar button for convenient access to settings. If it's not needed, you can remove it via View » Toolbars » Toolbar Layout
94 |
95 | **Bug Fixes**
96 | * In some cases, attachments can be saved under wrong name and wrong format. \[issue #364\]
97 | I added some manual correction to fix the problem. As this is a bug in the messages API, that returns an incorrectly encoded file name (it should decode the file name and return it correctly), so I raised [Bug 1992976](https://bugzilla.mozilla.org/show_bug.cgi?id=1992976) in this matter.
98 |
99 |
100 |
101 | 5.5.1 - published 17/10/2025
102 |
103 | **Improvements**
104 | * Made compatible with Thunderbird 145.*.
105 |
106 | **Bug Fixes**
107 | * Fixed Regression from 5.5: Attachments aren't saved anymore since update (Thunderbird 128). [issue #367], [issue #368]
108 |
109 |
110 |
111 | 6.0
112 |
113 | Increased strict_min_version to 140.0.
114 |
115 | **Improvements**
116 | * Exclude signatures from saved / detached attachments [issue #372]
117 | **Bug Fixes**
118 | * Problems with saving some attachments - others work [issue #376]
119 | * Tb142 removed `messenger.detachAttachmentsWOPrompts` - reimplement detach attachments [issue #369]
120 | * Fixed: Double saving via the filter as pdf and additional txt file [issue #370]
121 | * Removed legacy settings XUL dialog options.xhtml. [issue #379]
122 | * Added dedicated change log menu item. Added version numbers [issue #379]
123 |
124 |
125 | ======================================
126 |
127 | **TO DO NEXT**
128 | * #377 "Folder Name" Search Term no longer working with newer TB
129 | * Feature Request: Notification alert \[issue #240\].
130 | * Support custom file names, including date, when saving / detaching attachments [issue #219]
131 | * Test attachRegEx_match and see if it needs updates for Tb128 / Release
132 |
--------------------------------------------------------------------------------
/html/fq-message.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | /*
3 | BEGIN LICENSE BLOCK
4 |
5 | This file is part of FiltaQuilla, Custom Filter Actions
6 | rereleased by Axel Grude (original project by R Kent James
7 | under the Mesquilla Project)
8 |
9 | /FiltaQuilla is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation, either version 3 of the License, or
12 | (at your option) any later version.
13 |
14 | You should have received a copy of the GNU General Public License
15 | along with FiltaQuilla. If not, see .
16 |
17 | Software distributed under the License is distributed on an "AS IS" basis,
18 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
19 | for the specific language governing rights and limitations under the
20 | License.
21 |
22 | END LICENSE BLOCK
23 | */
24 |
25 | /*
26 | globals
27 | insertLocalizedMessage,
28 | formatAll,
29 | resizeWindow,
30 | i18n
31 | */
32 |
33 | function showElements(buttonList) {
34 | const buttons = buttonList.map((s) => s.trim());
35 | ["ok", "yes", "no", "cancel", "restart", "changeLog"].forEach((id) => {
36 | const el = document.getElementById(id);
37 | if (!el) {
38 | return;
39 | }
40 | el.hidden = !buttons.includes(id);
41 | });
42 | }
43 |
44 | // Helper to get query parameters
45 | function getQueryParams() {
46 | return Object.fromEntries(new URLSearchParams(window.location.search));
47 | }
48 |
49 | // helper to marshall a formatted message without using
50 | // queryParameter directly!
51 | // this consumes the message from local storage to avoid accidentally reusing it
52 | async function getStoredMessage(key, hasMessage) {
53 | if (!hasMessage) {
54 | return "";
55 | }
56 | try {
57 | const result = await browser.storage.local.get(key);
58 | await browser.storage.local.remove(key);
59 | if (result && typeof result === "object" && key in result) {
60 | return result[key] || "";
61 | }
62 | return `We seem to be missing a stored message in ${key}`;
63 | } catch (e) {
64 | console.error("Failed to get or remove message from storage", e);
65 | return "getStoredMessage failed!";
66 | }
67 | }
68 |
69 | window.addEventListener("load", async () => {
70 | const MESSAGE_STORAGE_KEY = "FiltaQuilla_Message_Key";
71 | const HEADING_STORAGE_KEY = "FiltaQuilla_Heading_Key";
72 |
73 | const params = getQueryParams();
74 | const features = (params.features || "ok").split(","); // fallback to "ok"
75 | /**** Passed Message or message id(s) to retrieve from l10n ****/
76 | // retrieve an arbitrary message text from storagem
77 | // but only if the queryparameter msg_storage was set!
78 | let message = await getStoredMessage(MESSAGE_STORAGE_KEY, !!params.msg_storage);
79 | const heading = await getStoredMessage(HEADING_STORAGE_KEY, !!params.msg_header_stored);
80 |
81 | if (heading) {
82 | document.getElementById("titleBox").textContent = heading;
83 | document.title = heading;
84 | }
85 |
86 | document.getElementById("changeLog").textContent = messenger.i18n.getMessage(
87 | "message.btn.changeLog", ""
88 | );
89 |
90 | let messageIdList = [];
91 | if (params.msgId) {
92 | // allow adding multiple ids as a comma separated string of localized message ids
93 | messageIdList =
94 | typeof params.msgId === "string" && params.msgId.includes(",")
95 | ? params.msgId.split(",").map((s) => s.trim())
96 | : [params.msgId];
97 |
98 | for (const id of messageIdList) {
99 | message += messenger.i18n.getMessage(id); // Each returns HTML with or {P1}{P2} as needed
100 | }
101 | }
102 | // we need to display _something_
103 | if (!message) {
104 | message = messenger.i18n.getMessage("message.placeholder");
105 | }
106 |
107 | // find all features relating to buttons:
108 | const buttonsList = features.filter((b) =>
109 | ["ok", "cancel", "yes", "no", "restart", "changeLog"].includes(b)
110 | );
111 |
112 | // Set message text
113 | const messageContainer = document.getElementById("innerMessage");
114 | // generate HTML markup
115 | await insertLocalizedMessage(messageContainer, message);
116 |
117 | i18n.updateDocument();
118 | showElements(buttonsList);
119 |
120 | // Show buttons according to features
121 | const elements = {
122 | ok: document.getElementById("ok"),
123 | yes: document.getElementById("yes"),
124 | no: document.getElementById("no"),
125 | cancel: document.getElementById("cancel"),
126 | changeLog: document.getElementById("changeLog"),
127 | // these are optional sections
128 | restart: document.getElementById("restart"),
129 | changeLogIntro: document.getElementById("changeLogIntro"),
130 | };
131 |
132 | if (messageIdList.includes("whats-new-list")) {
133 | elements.changeLogIntro?.removeAttribute("hidden");
134 | const introMsg = formatAll(browser.i18n.getMessage("whats-new-intro"));
135 | insertLocalizedMessage(elements.changeLogIntro, introMsg);
136 | }
137 |
138 | // Setup button handlers:
139 | elements.ok?.addEventListener("click", () => {
140 | messenger.runtime.sendMessage({ command: "filtaquilla-message", result: "ok" });
141 | });
142 | elements.cancel?.addEventListener("click", () => {
143 | messenger.runtime.sendMessage({ command: "filtaquilla-message", result: "cancel" });
144 | });
145 | elements.yes?.addEventListener("click", () => {
146 | messenger.runtime.sendMessage({ command: "filtaquilla-message", result: "yes" });
147 | });
148 | elements.no?.addEventListener("click", () => {
149 | messenger.runtime.sendMessage({ command: "filtaquilla-message", result: "no" });
150 | });
151 | elements.changeLog?.addEventListener("click", () => {
152 | messenger.runtime.sendMessage({ command: "filtaquilla-message", result: "changeLog" });
153 | });
154 |
155 | addEventListener("click", async (event) => {
156 | if (event.target.classList.contains("issue")) {
157 | let issueId = event.target.getAttribute("no");
158 | if (issueId) {
159 | event.preventDefault();
160 | messenger.windows.openDefaultBrowser(
161 | `https://github.com/RealRaven2000/FiltaQuilla/issues/${issueId}`
162 | );
163 | }
164 | }
165 | if (event.target.classList.contains("bug")) {
166 | let bugId = event.target.getAttribute("no");
167 | if (bugId) {
168 | event.preventDefault();
169 | messenger.windows.openDefaultBrowser(
170 | `https://bugzilla.mozilla.org/show_bug.cgi?id=${bugId}`
171 | );
172 | }
173 | }
174 | });
175 |
176 | // always allow hitting ESC to cancel
177 | window.addEventListener("keydown", function (event) {
178 | if (event.key === "Escape") {
179 | event.preventDefault();
180 | event.stopPropagation();
181 | messenger.runtime
182 | .sendMessage({ command: "filtaquilla-message", result: "cancel" })
183 | .finally(() => {
184 | // Delay close slightly to let browser finalize message
185 | setTimeout(() => window.close(), 150);
186 | });
187 | }
188 | });
189 |
190 | // make sure add-on links stay in Thunderbird!
191 | document.addEventListener("click", (event) => {
192 | const link = event.target.closest("a.native");
193 | if (link) {
194 | event.preventDefault();
195 | browser.tabs.create({ url: link.href });
196 | link.classList.add("link-visited");
197 | // Find or create the status span next to the link
198 | let status = link.nextElementSibling;
199 | if (!status || !status.classList.contains("link-visited")) {
200 | status = document.createElement("span");
201 | status.className = "link-visited";
202 | link.parentNode.insertBefore(status, link.nextSibling);
203 | }
204 | status.textContent = " " + messenger.i18n.getMessage("message.linkInTab");
205 | }
206 | });
207 | resizeWindow();
208 | });
209 |
--------------------------------------------------------------------------------
/content/api/LegacyPrefs/implementation.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is provided by the addon-developer-support repository at
3 | * https://github.com/thunderbird/addon-developer-support
4 | *
5 | * Version 1.12
6 | * - added createPref(), proposed by Axel Grude
7 | *
8 | * Version 1.11
9 | * - adjusted to TB128 (no longer loading Services and ExtensionCommon)
10 | * - use ChromeUtils.importESModule()
11 | *
12 | * Version 1.10
13 | * - adjusted to Thunderbird 115 (Services is now in globalThis)
14 | *
15 | * Version 1.9
16 | * - fixed fallback issue reported by Axel Grude
17 | *
18 | * Version 1.8
19 | * - reworked onChanged event to allow registering multiple branches
20 | *
21 | * Version 1.7
22 | * - add onChanged event
23 | *
24 | * Version 1.6
25 | * - add setDefaultPref()
26 | *
27 | * Version 1.5
28 | * - replace set/getCharPref by set/getStringPref to fix encoding issue
29 | *
30 | * Version 1.4
31 | * - setPref() function returns true if the value could be set, otherwise false
32 | *
33 | * Version 1.3
34 | * - add setPref() function
35 | *
36 | * Version 1.2
37 | * - add getPref() function
38 | *
39 | * Author: John Bieling (john@thunderbird.net)
40 | *
41 | * This Source Code Form is subject to the terms of the Mozilla Public
42 | * License, v. 2.0. If a copy of the MPL was not distributed with this
43 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
44 | */
45 |
46 | /* global Services, ExtensionCommon */
47 |
48 | "use strict";
49 |
50 | var { ExtensionUtils } = ChromeUtils.importESModule(
51 | "resource://gre/modules/ExtensionUtils.sys.mjs"
52 | );
53 | var { ExtensionError } = ExtensionUtils;
54 |
55 | var LegacyPrefs = class extends ExtensionCommon.ExtensionAPI {
56 | getAPI(context) {
57 |
58 | class LegacyPrefsManager {
59 | constructor() {
60 | this.observedBranches = new Map();
61 | this.QueryInterface = ChromeUtils.generateQI([
62 | "nsIObserver",
63 | "nsISupportsWeakReference",
64 | ])
65 | }
66 |
67 | addObservedBranch(branch, fire) {
68 | return this.observedBranches.set(branch, fire);
69 | }
70 |
71 | hasObservedBranch(branch) {
72 | return this.observedBranches.has(branch);
73 | }
74 |
75 | removeObservedBranch(branch) {
76 | return this.observedBranches.delete(branch);
77 | }
78 |
79 | async observe(aSubject, aTopic, aData) {
80 | if (aTopic == "nsPref:changed") {
81 | let branch = [...this.observedBranches.keys()]
82 | .reduce(
83 | (p, c) => aData.startsWith(c) && (!p || c.length > p.length) ? c : p,
84 | null
85 | );
86 | if (branch) {
87 | let name = aData.substr(branch.length);
88 | let value = await this.getLegacyPref(aData);
89 | let fire = this.observedBranches.get(branch);
90 | fire(name, value);
91 | }
92 | }
93 | }
94 |
95 | async getLegacyPref(
96 | aName,
97 | aFallback = null,
98 | userPrefOnly = true
99 | ) {
100 | let prefType = Services.prefs.getPrefType(aName);
101 | if (prefType == Services.prefs.PREF_INVALID) {
102 | return aFallback;
103 | }
104 |
105 | let value = aFallback;
106 | if (!userPrefOnly || Services.prefs.prefHasUserValue(aName)) {
107 | switch (prefType) {
108 | case Services.prefs.PREF_STRING:
109 | value = Services.prefs.getStringPref(aName, aFallback);
110 | break;
111 |
112 | case Services.prefs.PREF_INT:
113 | value = Services.prefs.getIntPref(aName, aFallback);
114 | break;
115 |
116 | case Services.prefs.PREF_BOOL:
117 | value = Services.prefs.getBoolPref(aName, aFallback);
118 | break;
119 |
120 | default:
121 | console.error(
122 | `Legacy preference <${aName}> has an unknown type of <${prefType}>.`
123 | );
124 | }
125 | }
126 | return value;
127 | }
128 | }
129 |
130 | let legacyPrefsManager = new LegacyPrefsManager();
131 |
132 | return {
133 | LegacyPrefs: {
134 | onChanged: new ExtensionCommon.EventManager({
135 | context,
136 | name: "LegacyPrefs.onChanged",
137 | register: (fire, branch) => {
138 | if (legacyPrefsManager.hasObservedBranch(branch)) {
139 | throw new ExtensionError(`Cannot add more than one listener for branch "${branch}".`)
140 | }
141 | legacyPrefsManager.addObservedBranch(branch, fire.sync);
142 | Services.prefs
143 | .getBranch(null)
144 | .addObserver(branch, legacyPrefsManager);
145 | return () => {
146 | Services.prefs
147 | .getBranch(null)
148 | .removeObserver(branch, legacyPrefsManager);
149 | legacyPrefsManager.removeObservedBranch(branch);
150 | };
151 | },
152 | }).api(),
153 |
154 | // only returns something, if a user pref value is set
155 | getUserPref: async function (aName) {
156 | return await legacyPrefsManager.getLegacyPref(aName);
157 | },
158 |
159 | // returns the default value, if no user defined value exists,
160 | // and returns the fallback value, if the preference does not exist
161 | getPref: async function (aName, aFallback = null) {
162 | return await legacyPrefsManager.getLegacyPref(aName, aFallback, false);
163 | },
164 |
165 | clearUserPref: function (aName) {
166 | Services.prefs.clearUserPref(aName);
167 | },
168 |
169 | // creates a new pref
170 | createPref: async function (aName, aValue) {
171 | if (typeof aValue == "string") {
172 | Services.prefs.setStringPref(aName, aValue);
173 | return "string";
174 | }
175 |
176 | if (typeof aValue == "boolean") {
177 | Services.prefs.setBoolPref(aName, aValue);
178 | return "boolean";
179 | }
180 |
181 | if (typeof aValue == "number" && Number.isSafeInteger(aValue)) {
182 | Services.prefs.setIntPref(aName, aValue);
183 | return "integer";
184 | }
185 | console.error(
186 | `The provided value <${aValue}> for the new legacy preference <${aName}> is none of STRING, BOOLEAN or INTEGER.`
187 | );
188 | return false;
189 | },
190 |
191 | // sets a pref
192 | setPref: async function (aName, aValue) {
193 | let prefType = Services.prefs.getPrefType(aName);
194 | if (prefType == Services.prefs.PREF_INVALID) {
195 | console.error(
196 | `Unknown legacy preference <${aName}>, forgot to declare a default?.`
197 | );
198 | return false;
199 | }
200 |
201 | switch (prefType) {
202 | case Services.prefs.PREF_STRING:
203 | Services.prefs.setStringPref(aName, aValue);
204 | return true;
205 | break;
206 |
207 | case Services.prefs.PREF_INT:
208 | Services.prefs.setIntPref(aName, aValue);
209 | return true;
210 | break;
211 |
212 | case Services.prefs.PREF_BOOL:
213 | Services.prefs.setBoolPref(aName, aValue);
214 | return true;
215 | break;
216 |
217 | default:
218 | console.error(
219 | `Legacy preference <${aName}> has an unknown type of <${prefType}>.`
220 | );
221 | }
222 | return false;
223 | },
224 |
225 | setDefaultPref: async function (aName, aValue) {
226 | let defaults = Services.prefs.getDefaultBranch("");
227 | switch (typeof aValue) {
228 | case "string":
229 | return defaults.setStringPref(aName, aValue);
230 | case "number":
231 | return defaults.setIntPref(aName, aValue);
232 | case "boolean":
233 | return defaults.setBoolPref(aName, aValue);
234 | }
235 | },
236 | },
237 | };
238 | }
239 | };
240 |
--------------------------------------------------------------------------------
/_locales/ja/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "aboutAndSupport": {
3 | "message": "About FiltaQuilla / Help"
4 | },
5 | "applyIncomingMails": {
6 | "message": "Run filters on incoming mails"
7 | },
8 | "aria.configSettings": {
9 | "message": "Advanced configuration - for additional settings."
10 | },
11 | "checkJavascriptActionBodyEnabled.accesskey": {
12 | "message": "Y"
13 | },
14 | "checkJavascriptActionBodyEnabled.label": {
15 | "message": "Javascript Action with Body"
16 | },
17 | "checkJavascriptEnabled.accesskey": {
18 | "message": "J"
19 | },
20 | "checkJavascriptEnabled.label": {
21 | "message": "Javascript"
22 | },
23 | "default": {
24 | "message": "Use default"
25 | },
26 | "editJavascript": {
27 | "message": "Edit Javascript"
28 | },
29 | "enabled": {
30 | "message": "Enabled"
31 | },
32 | "extensionDescription": {
33 | "description": "Description of the extension.",
34 | "message": "Quickly generate mail filters on the fly, by dragging and dropping mails and analyzing their attributes."
35 | },
36 | "extensionName": {
37 | "description": "Name of the extension.",
38 | "message": "FiltaQuilla"
39 | },
40 | "extensions.filtaquilla.description": {
41 | "message": "Mail filter custom actions"
42 | },
43 | "feature.unsupported": {
44 | "message": "This feature is currently not supported."
45 | },
46 | "filtaquilla.applyIncomingFilters": {
47 | "message": "Apply Incoming Filters"
48 | },
49 | "filtaquilla.applyIncomingFilters.accesskey": {
50 | "message": "F"
51 | },
52 | "filtaquilla.editJavascript": {
53 | "message": "Edit JavaScript…"
54 | },
55 | "filtaquilla.enabled": {
56 | "message": "Enabled"
57 | },
58 | "filtaquilla.filters": {
59 | "message": "Filters"
60 | },
61 | "filtaquilla.inherit": {
62 | "message": "Inherit"
63 | },
64 | "filtaquilla.javascriptAction.name": {
65 | "message": "Javascript Action"
66 | },
67 | "filtaquilla.javascriptActionBody.name": {
68 | "message": "Javascript Action with Body"
69 | },
70 | "filtaquilla.launcher.launch": {
71 | "message": "Launch the File!"
72 | },
73 | "filtaquilla.launcher.select": {
74 | "message": "Select a File…"
75 | },
76 | "filtaquilla.playSound": {
77 | "message": "Play Sound"
78 | },
79 | "filtaquilla.runProgram.select": {
80 | "message": "Select a Program…"
81 | },
82 | "filtaquilla.runProgram.title": {
83 | "message": "Select a Program to run"
84 | },
85 | "filtaquilla.selectFolder.btn": {
86 | "message": "Pick Folder…"
87 | },
88 | "filtaquilla.selectFolder.title": {
89 | "message": "Select a Folder"
90 | },
91 | "filtaquilla.template.select": {
92 | "message": "Select a Template…"
93 | },
94 | "filtaquilla.tone.select": {
95 | "message": "Select a Sound File…"
96 | },
97 | "filterActions": {
98 | "message": "Filter Actions"
99 | },
100 | "fq.Bcc": {
101 | "message": "Bcc"
102 | },
103 | "fq.Bcc.access": {
104 | "message": "B"
105 | },
106 | "fq.addSender": {
107 | "message": "Add Sender to Address List"
108 | },
109 | "fq.addSender.access": {
110 | "message": "L"
111 | },
112 | "fq.attachmentRegex": {
113 | "message": "Attachment Name Regex Match"
114 | },
115 | "fq.bodyRegex": {
116 | "message": "Body Regexp match"
117 | },
118 | "fq.copyAsRead": {
119 | "message": "Copy As Read"
120 | },
121 | "fq.copyAsRead.access": {
122 | "message": "C"
123 | },
124 | "fq.detachAttachments": {
125 | "message": "Detach Attachments To"
126 | },
127 | "fq.detachAttachments.access": {
128 | "message": "D"
129 | },
130 | "fq.filtaquilla.mustSelectFolder": {
131 | "message": "You must select a target folder"
132 | },
133 | "fq.fileNameMaxLength": {
134 | "message": "Maximal length"
135 | },
136 | "fq.fileNameSpaceCharacter": {
137 | "message": "Replace spaces in file names with"
138 | },
139 | "fq.fileNameWhiteList": {
140 | "message": "Whitelisted letters"
141 | },
142 | "fq.folderName": {
143 | "message": "Folder Name"
144 | },
145 | "fq.folderName.access": {
146 | "message": "F"
147 | },
148 | "fq.hdrRegex": {
149 | "message": "Header Regex Match"
150 | },
151 | "fq.hdrRegex.access": {
152 | "message": "H"
153 | },
154 | "fq.javascript": {
155 | "message": "Javascript"
156 | },
157 | "fq.javascriptAction": {
158 | "message": "Javascriptアクション"
159 | },
160 | "fq.javascriptAction.access": {
161 | "message": "V"
162 | },
163 | "fq.launchFile": {
164 | "message": "ファイルを起動"
165 | },
166 | "fq.launchFile.access": {
167 | "message": "F"
168 | },
169 | "fq.markReplied": {
170 | "message": "返信済みにマーク"
171 | },
172 | "fq.markReplied.access": {
173 | "message": "E"
174 | },
175 | "fq.markUnread": {
176 | "message": "未読にマーク"
177 | },
178 | "fq.markUnread.access": {
179 | "message": "M"
180 | },
181 | "fq.moveLater": {
182 | "message": "後で移動"
183 | },
184 | "fq.moveLater.access": {
185 | "message": "A"
186 | },
187 | "fq.nobiff": {
188 | "message": "通知しない"
189 | },
190 | "fq.nobiff.access": {
191 | "message": "N"
192 | },
193 | "fq.print": {
194 | "message": "印刷"
195 | },
196 | "fq.print.access": {
197 | "message": "P"
198 | },
199 | "fq.printAllowDuplicates": {
200 | "message": "重複を許可"
201 | },
202 | "fq.printTools": {
203 | "message": "PrintingTools NGを使用して印刷"
204 | },
205 | "fq.regex.caseInsensitive": {
206 | "message": "正規表現 大文字小文字を区別しない"
207 | },
208 | "fq.regex.multilineanchors": {
209 | "message": "アドレスヘッダーで '^' および '$' のアンカーをサポート"
210 | },
211 | "fq.removeflagged": {
212 | "message": "スターを削除"
213 | },
214 | "fq.removeflagged.access": {
215 | "message": "S"
216 | },
217 | "fq.removekeyword": {
218 | "message": "タグを削除"
219 | },
220 | "fq.removekeyword.access": {
221 | "message": "O"
222 | },
223 | "fq.runFile": {
224 | "message": "プログラムを実行"
225 | },
226 | "fq.runFile.access": {
227 | "message": "R"
228 | },
229 | "fq.runFile.unicode": {
230 | "message": "UTF-16をデフォルトに"
231 | },
232 | "fq.runFile.unicode.tooltip": {
233 | "message": "個別エンコーディングを使用するには @UTF16@ または @UTF8@ パラメータを使用"
234 | },
235 | "fq.saveAttachment": {
236 | "message": "添付ファイルを保存"
237 | },
238 | "fq.saveAttachment.access": {
239 | "message": "H"
240 | },
241 | "fq.saveMsgAsFile": {
242 | "message": "メッセージをファイルとして保存"
243 | },
244 | "fq.saveMsgAsFile.access": {
245 | "message": "I"
246 | },
247 | "fq.smartTemplate.fwd": {
248 | "message": "SmartTemplateで転送"
249 | },
250 | "fq.smartTemplate.rsp": {
251 | "message": "SmartTemplateで返信"
252 | },
253 | "fq.archiveMessage": {
254 | "message": "メッセージをアーカイブ"
255 | },
256 | "fq.subjectBodyRegex": {
257 | "message": "件名または本文の正規表現マッチ"
258 | },
259 | "fq.subjectRegex": {
260 | "message": "件名の正規表現マッチ"
261 | },
262 | "fq.subjectRegex.access": {
263 | "message": "S"
264 | },
265 | "fq.subjectappend": {
266 | "message": "件名に追加"
267 | },
268 | "fq.subjectappend.access": {
269 | "message": "A"
270 | },
271 | "fq.subjectprepend": {
272 | "message": "件名に前置き"
273 | },
274 | "fq.subjectprepend.access": {
275 | "message": "U"
276 | },
277 | "fq.threadAnyTag": {
278 | "message": "スレッドメッセージのタグ"
279 | },
280 | "fq.threadAnyTag.access": {
281 | "message": "T"
282 | },
283 | "fq.threadHeadTag": {
284 | "message": "スレッドの先頭タグ"
285 | },
286 | "fq.threadHeadTag.access": {
287 | "message": "H"
288 | },
289 | "fq.toneQuilla": {
290 | "message": "サウンド再生(Tonequilla)"
291 | },
292 | "fq.trainGood": {
293 | "message": "良いとして学習"
294 | },
295 | "fq.trainGood.access": {
296 | "message": "G"
297 | },
298 | "fq.trainJunk": {
299 | "message": "迷惑として学習"
300 | },
301 | "fq.trainJunk.access": {
302 | "message": "J"
303 | },
304 | "githubPage": {
305 | "message": "FiltaQuillaのGitHubページを開く"
306 | },
307 | "helpFeatureTip": {
308 | "message": "この機能の詳細を表示"
309 | },
310 | "indexInGlobalDatabase": {
311 | "message": "グローバルデータベースにインデックス"
312 | },
313 | "inherit": {
314 | "message": "継承"
315 | },
316 | "inheritedProperties": {
317 | "message": "継承されたプロパティ"
318 | },
319 | "message.btn.accept": { "message": "OK" },
320 | "message.btn.cancel": { "message": "キャンセル" },
321 | "message.btn.changeLog": {
322 | "message": "最新の変更を表示 $versionPart$…",
323 | "placeholders": { "versionPart": { "content": "$1" } }
324 | },
325 | "message.btn.no": { "message": "いいえ" },
326 | "message.btn.yes": { "message": "はい" },
327 | "message.linkInTab": { "message": "(リンクはタブで開きます)" },
328 | "message.placeholder": { "message": "メッセージがありません。" },
329 | "message.restart": {
330 | "message": "FiltaQuillaは一部のカスタムフィルターアクションを変更しました。これらの変更は次回Thunderbird起動時にのみ適用されます。"
331 | },
332 | "newsHead": { "message": "最新情報" },
333 | "newsMsgForced": {
334 | "message": "{P}このバージョンでは、FiltaQuilla の Thunderbird 最低動作バージョンを引き上げ、添付ファイルの信頼性のある処理と長期的な安定性を確保しています。{bold}FiltaQuilla 6.*{/bold} 以降のバージョンでは {bold}Thunderbird 140{/bold} 以上が必要です。{/P}{P}古い Thunderbird バージョンでは、一部のユーザーで添付ファイルの保存が不安定でした。このアップデートでは、可能な限りこれらの問題を修正し、全体的な安定性を向上させています。{/P}{P}Thunderbird は最近のリリースで多くの内部変更があり、古いバージョンをサポートし続けると、もはや一貫性のない複製パスを維持する必要がありました。今後は FiltaQuilla を安定して動作させることができます。{/P}{P}最新バージョンに更新し、FiltaQuilla を将来にわたって維持可能にするためにご協力いただきありがとうございます。{/P}"
335 | },
336 | "optionsIntro": {
337 | "message": "'フィルタールール' ダイアログで利用可能にしたい項目をすべて有効にしてください。変更後は再起動してください。"
338 | },
339 | "pane1.title": {
340 | "message": "FiltaQuillaの設定"
341 | },
342 | "prefPanel-inheritPane": {
343 | "message": "継承プロパティ"
344 | },
345 | "prefwindow.title": {
346 | "message": "FiltaQuilla設定"
347 | },
348 | "purchase-a-license": {
349 | "message": "ライセンスを購入する"
350 | },
351 | "purchase-heading": {
352 | "message": "ライセンスの購入"
353 | },
354 | "quickFiltersPage": {
355 | "message": "quickFiltersを入手"
356 | },
357 | "quickFiltersPage_desc": {
358 | "message": "この素晴らしいアドオンをダウンロードしてフィルタリングを加速します。quickFiltersはフィルター定義と管理を簡単にします。"
359 | },
360 | "regex.accept": {
361 | "message": "承認"
362 | },
363 | "regex.btnBuilder.tooltip": {
364 | "message": "正規表現をテストするにはここをクリック"
365 | },
366 | "regex.cancel": {
367 | "message": "キャンセル"
368 | },
369 | "regex.collapseWhiteSpace": {
370 | "message": "空白をすべて折りたたむ"
371 | },
372 | "regex.contentfilter": {
373 | "message": "コンテンツ制限:"
374 | },
375 | "regex.content.plaintext": {
376 | "message": "プレーンテキスト部分を処理"
377 | },
378 | "regex.content.html": {
379 | "message": "HTML部分を処理"
380 | },
381 | "regex.content.raw": {
382 | "message": "生データモード: プレーンテキスト部分は前処理なし"
383 | },
384 | "regex.content.vcard": {
385 | "message": "vCard部分を処理"
386 | },
387 | "regex.exclude.html": {
388 | "message": "すべてのHTMLタグを除外"
389 | },
390 | "regex.exclude.quotes": {
391 | "message": "引用符を除外(HTML+プレーンテキスト)"
392 | },
393 | "regex.exclude.style": {
394 | "message": "グローバル