├── locale ├── en │ ├── bindings.dtd │ ├── am-inheritPane.properties │ └── am-inheritPane.dtd ├── nl │ ├── bindings.dtd │ ├── am-inheritPane.properties │ └── am-inheritPane.dtd ├── sv │ ├── bindings.dtd │ ├── am-inheritPane.properties │ └── am-inheritPane.dtd ├── de │ ├── bindings.dtd │ ├── am-inheritPane.properties │ └── am-inheritPane.dtd └── ru │ ├── bindings.dtd │ ├── am-inheritPane.properties │ └── am-inheritPane.dtd ├── skin ├── js-editor.css ├── folder.png ├── script.png ├── community.png ├── folder_go.png ├── filtaQuilla.png ├── quickfilters.png ├── script_edit.png ├── filtaquilla-32.png ├── filtaquilla-64.png ├── fugue-question.png ├── fugue-arrow-curve.png ├── fugue-question-disabled.png ├── media-play.svg ├── help.svg ├── help-disabled.svg ├── pencil.svg ├── changelog.svg ├── settings-simple.svg ├── tooltip.svg ├── settings.svg ├── filtaquilla.css ├── github.svg ├── help-tip.svg ├── new.svg └── filtaquilla-prefs.css ├── content ├── sounds │ ├── Freedom.ogg │ ├── pour-1.wav │ ├── pour-2.ogg │ ├── squeak.wav │ ├── applause.ogg │ ├── duogourd.ogg │ ├── knob-458.ogg │ ├── notify-1.wav │ ├── nightingale.ogg │ ├── scissors-423.ogg │ ├── scratch-389.ogg │ ├── squishbeat.ogg │ ├── worthwhile-438.ogg │ ├── your-turn-491.ogg │ ├── TheBrightestStar.ogg │ ├── maybe-one-day-584.ogg │ ├── notification-squeak.wav │ └── hold-your-horses-468.ogg ├── api │ ├── WindowListener │ │ ├── README.md │ │ ├── schema.json │ │ └── changelog.md │ ├── DomContentScript │ │ ├── schema.json │ │ └── implementation.js │ ├── NotifyTools │ │ ├── schema.json │ │ ├── implementation.js │ │ └── README.md │ ├── FiltaQuilla │ │ ├── schema.json │ │ └── implementation.js │ └── LegacyPrefs │ │ ├── README.md │ │ ├── schema.json │ │ └── implementation.js ├── scripts │ ├── filtaquilla-filterEditor-css.js │ ├── filtaquilla-messenger.js │ ├── filtaquilla-folderProps.js │ └── notifyTools.js ├── jsEditor.html ├── i18n.js ├── jsEditor.js └── folderPropsOverlay.js ├── html ├── fq-main.js ├── fq-dialogs.css ├── fq-message.html ├── fq-util.js ├── fq-message.css ├── popup.js ├── fq-settings.js └── fq-message.js ├── revision.bat ├── filtaquilla-background.html ├── license-artwork.txt ├── .gitignore ├── readme.md ├── .vscode └── settings.json ├── .eslintrc.cjs ├── release-notes.md ├── manifest.json ├── release-notes.html ├── defaults └── preferences │ └── filtaquilla.js ├── changes.txt └── _locales ├── ja └── messages.json ├── sv └── messages.json ├── ru └── messages.json └── en └── messages.json /locale/en/bindings.dtd: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /locale/nl/bindings.dtd: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /locale/sv/bindings.dtd: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /locale/de/bindings.dtd: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /locale/ru/bindings.dtd: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /skin/js-editor.css: -------------------------------------------------------------------------------- 1 | 2 | #jscode { 3 | min-height: 9em; 4 | vertical-align: top; 5 | } -------------------------------------------------------------------------------- /skin/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/skin/folder.png -------------------------------------------------------------------------------- /skin/script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/skin/script.png -------------------------------------------------------------------------------- /skin/community.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/skin/community.png -------------------------------------------------------------------------------- /skin/folder_go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/skin/folder_go.png -------------------------------------------------------------------------------- /skin/filtaQuilla.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/skin/filtaQuilla.png -------------------------------------------------------------------------------- /skin/quickfilters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/skin/quickfilters.png -------------------------------------------------------------------------------- /skin/script_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/skin/script_edit.png -------------------------------------------------------------------------------- /skin/filtaquilla-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/skin/filtaquilla-32.png -------------------------------------------------------------------------------- /skin/filtaquilla-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/skin/filtaquilla-64.png -------------------------------------------------------------------------------- /skin/fugue-question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/skin/fugue-question.png -------------------------------------------------------------------------------- /content/sounds/Freedom.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/content/sounds/Freedom.ogg -------------------------------------------------------------------------------- /content/sounds/pour-1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/content/sounds/pour-1.wav -------------------------------------------------------------------------------- /content/sounds/pour-2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/content/sounds/pour-2.ogg -------------------------------------------------------------------------------- /content/sounds/squeak.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/content/sounds/squeak.wav -------------------------------------------------------------------------------- /skin/fugue-arrow-curve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/skin/fugue-arrow-curve.png -------------------------------------------------------------------------------- /content/sounds/applause.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/content/sounds/applause.ogg -------------------------------------------------------------------------------- /content/sounds/duogourd.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/content/sounds/duogourd.ogg -------------------------------------------------------------------------------- /content/sounds/knob-458.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/content/sounds/knob-458.ogg -------------------------------------------------------------------------------- /content/sounds/notify-1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/content/sounds/notify-1.wav -------------------------------------------------------------------------------- /locale/sv/am-inheritPane.properties: -------------------------------------------------------------------------------- 1 | prefPanel-inheritPane=Ärvda egenskaper 2 | inherit=Ärv 3 | enabled=Aktiverad 4 | -------------------------------------------------------------------------------- /content/sounds/nightingale.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/content/sounds/nightingale.ogg -------------------------------------------------------------------------------- /content/sounds/scissors-423.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/content/sounds/scissors-423.ogg -------------------------------------------------------------------------------- /content/sounds/scratch-389.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/content/sounds/scratch-389.ogg -------------------------------------------------------------------------------- /content/sounds/squishbeat.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/content/sounds/squishbeat.ogg -------------------------------------------------------------------------------- /locale/en/am-inheritPane.properties: -------------------------------------------------------------------------------- 1 | prefPanel-inheritPane=Inherited Properties 2 | inherit=Inherit 3 | enabled=Enabled 4 | -------------------------------------------------------------------------------- /content/sounds/worthwhile-438.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/content/sounds/worthwhile-438.ogg -------------------------------------------------------------------------------- /content/sounds/your-turn-491.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/content/sounds/your-turn-491.ogg -------------------------------------------------------------------------------- /html/fq-main.js: -------------------------------------------------------------------------------- 1 | console.log("Loading fq-main.js"); 2 | // eslint-disable-next-line no-unused-vars 3 | var FiltaQuilla = {}; 4 | -------------------------------------------------------------------------------- /locale/de/am-inheritPane.properties: -------------------------------------------------------------------------------- 1 | prefPanel-inheritPane=Übernommene Eigenschaften 2 | inherit=Übernehmen 3 | enabled=Aktiv 4 | -------------------------------------------------------------------------------- /skin/fugue-question-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/skin/fugue-question-disabled.png -------------------------------------------------------------------------------- /content/sounds/TheBrightestStar.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/content/sounds/TheBrightestStar.ogg -------------------------------------------------------------------------------- /content/sounds/maybe-one-day-584.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/content/sounds/maybe-one-day-584.ogg -------------------------------------------------------------------------------- /locale/ru/am-inheritPane.properties: -------------------------------------------------------------------------------- 1 | prefPanel-inheritPane=Унаследованные свойства 2 | inherit=Унаследованные 3 | enabled=Включено 4 | -------------------------------------------------------------------------------- /content/sounds/notification-squeak.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/content/sounds/notification-squeak.wav -------------------------------------------------------------------------------- /locale/nl/am-inheritPane.properties: -------------------------------------------------------------------------------- 1 | prefPanel-inheritPane=Overgenomen eigenschappen 2 | inherit=Overnemen 3 | enabled=Ingeschakeld 4 | -------------------------------------------------------------------------------- /content/sounds/hold-your-horses-468.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealRaven2000/FiltaQuilla/HEAD/content/sounds/hold-your-horses-468.ogg -------------------------------------------------------------------------------- /html/fq-dialogs.css: -------------------------------------------------------------------------------- 1 | .hbox, .vbox { 2 | display: flex; 3 | } 4 | .hbox { 5 | flex-direction: row; 6 | } 7 | .vbox { 8 | flex-direction: column; 9 | } 10 | -------------------------------------------------------------------------------- /skin/media-play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /locale/en/am-inheritPane.dtd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /locale/sv/am-inheritPane.dtd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /revision.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | 4 | set MYDIR=%~dp0 5 | set BKPDIR=%MYDIR%\..\..\_Test Versions\5.4\ 6 | 7 | move *.xpi "%BKPDIR%">NUL 8 | powershell -Version 3 -File "%MYDIR%\build.ps1" -IncrementRevision 9 | -------------------------------------------------------------------------------- /locale/nl/am-inheritPane.dtd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /locale/ru/am-inheritPane.dtd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /filtaquilla-background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /locale/de/am-inheritPane.dtd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /content/api/WindowListener/README.md: -------------------------------------------------------------------------------- 1 | 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). 2 | -------------------------------------------------------------------------------- /content/scripts/filtaquilla-filterEditor-css.js: -------------------------------------------------------------------------------- 1 | /* 2 | globals 3 | WL, 4 | */ 5 | 6 | // eslint-disable-next-line no-unused-vars 7 | function onLoad(activatedWhileWindowOpen) { 8 | // eslint-disable-next-line no-unused-vars 9 | let _layout2 = WL.injectCSS("resource://filtaquilla-skin/filtaquilla.css"); 10 | } 11 | 12 | -------------------------------------------------------------------------------- /license-artwork.txt: -------------------------------------------------------------------------------- 1 | github.svn: https://www.svgrepo.com/svg/394174/github 2 | Github SVG Vector 43 3 | 4 | Free Download Github 43 SVG vector file in monocolor and multicolor type for Sketch and Figma from Github 43 Vectors svg vector collection. Github 43 Vectors SVG vector illustration graphic art design format. 5 | 6 | COLLECTION: Fontsio Interface Icons 7 | LICENSE: MIT License 8 | AUTHOR: Kenan Gundogan 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ############################################################################# 2 | /build 3 | /tmp 4 | 5 | ############################################################################# 6 | # Ignore editor junk 7 | *.swp 8 | *.bkp 9 | *.bak 10 | *~ 11 | .~lock.* 12 | 13 | ############################################################################# 14 | # Ignore files used/generated by package builder(s) 15 | /revision.txt 16 | *.xpi 17 | *.zip 18 | *.log 19 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # FiltaQuilla 2 | Adds many new mail filter actions - launch a file, suppress notification, remove star or tag, mark replied or unread, copy as "read", append text to subject. 3 | 4 | 07/Nov/2018 5 | 6 | This Add-on is adopted from its previous author & creator R. Kent James who 7 | had to retire form Add-on development. As I am often asked about added features 8 | for filter conditions and actions for my Add-on quickFilters this is a good 9 | container for this additional functionality. I am keeping the license as GPL 3.0 for now. 10 | 11 | 13/10/2025 12 | 13 | Added external icons, see list in license-artwork.txt -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.background": "#8a9703", 4 | "activityBar.inactiveForeground": "#f9f984", 5 | "titleBar.activeBackground": "#7d8804", 6 | "statusBar.background": "#73930b", 7 | "menu.separatorBackground": "#08661c", 8 | "menu.selectionBackground": "#03750d", 9 | "menubar.selectionBackground": "#ff0001" 10 | }, 11 | "eslint.validate": ["javascript"], 12 | "eslint.codeActionsOnSave.rules": null, 13 | "eslint.run": "onType", 14 | "eslint.options": { 15 | "overrideConfigFile": "E:/Dev/Mozilla/Dev/FiltaQuilla/_Github/branches/ESR128/.eslintrc.cjs" 16 | } 17 | } -------------------------------------------------------------------------------- /content/api/DomContentScript/schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "namespace": "DomContentScript", 4 | "functions": [ 5 | { 6 | "name": "registerWindow", 7 | "type": "function", 8 | "async": true, 9 | "description": "Register a script for onDOMContentLoaded", 10 | "parameters": [ 11 | { 12 | "name": "windowUrl", 13 | "type": "string", 14 | "description": "chrome URL of the window " 15 | }, 16 | { 17 | "name": "jsPath", 18 | "type": "string", 19 | "description": "chrome URL of the script" 20 | } 21 | ] 22 | } 23 | ] 24 | } 25 | ] 26 | -------------------------------------------------------------------------------- /content/scripts/filtaquilla-messenger.js: -------------------------------------------------------------------------------- 1 | Services.scriptloader.loadSubScript("chrome://filtaquilla/content/filtaquilla-util.js", window, "UTF-8"); 2 | Services.scriptloader.loadSubScript("chrome://filtaquilla/content/filtaquilla.js", window, "UTF-8"); 3 | 4 | function onLoad(activatedWhileWindowOpen) { 5 | window.filtaquilla.onLoad(); // do we get an event to pass? 6 | } 7 | 8 | function onUnload(isAddOnShutown) { 9 | const Cc = Components.classes, 10 | Ci = Components.interfaces, 11 | filterService = Cc["@mozilla.org/messenger/services/filters;1"].getService(Ci.nsIMsgFilterService); 12 | 13 | 14 | // filterService 15 | let ca = []; 16 | ca = [...filterService.getCustomActions()]; 17 | 18 | 19 | } 20 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2024: true, 5 | node: true, 6 | webextensions: true, 7 | }, 8 | globals: { 9 | browser: "readonly", 10 | messenger: "readonly", 11 | ChromeUtils: "readonly", 12 | Components: "readonly", 13 | IOUtils: "readonly", 14 | PathUtils: "readonly", 15 | PrintUtils: "readonly", 16 | NetUtil: "readonly", 17 | Services: "readonly", 18 | FiltaQuilla: "readonly", 19 | gMessageDisplay: "readonly", 20 | gFolderDisplay: "readonly", 21 | }, 22 | extends: ["eslint:recommended"], 23 | parserOptions: { 24 | ecmaVersion: 2022, 25 | sourceType: "module", 26 | }, 27 | rules: { 28 | // your rules here 29 | "no-const-assign": "error", 30 | "no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], 31 | "no-undef": "error", 32 | "no-redeclare": "error", 33 | eqeqeq: "off", 34 | curly: "warn", 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /content/jsEditor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Edit JavaScript 6 | 27 | 28 | 29 | 30 | 31 |
32 | 33 | 34 |
35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /content/api/NotifyTools/schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "namespace": "NotifyTools", 4 | "events": [ 5 | { 6 | "name": "onNotifyBackground", 7 | "type": "function", 8 | "description": "Fired when a new notification from notifyTools.js in an Experiment has been received.", 9 | "parameters": [ 10 | { 11 | "name": "data", 12 | "type": "any", 13 | "description": "Restrictions of the structured clone algorithm apply." 14 | } 15 | ] 16 | } 17 | ], 18 | "functions": [ 19 | { 20 | "name": "notifyExperiment", 21 | "type": "function", 22 | "async": true, 23 | "description": "Notifies notifyTools.js in an Experiment and sends data.", 24 | "parameters": [ 25 | { 26 | "name": "data", 27 | "type": "any", 28 | "description": "Restrictions of the structured clone algorithm apply." 29 | } 30 | ] 31 | } 32 | ] 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /skin/help.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | 23 | -------------------------------------------------------------------------------- /skin/help-disabled.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | 23 | -------------------------------------------------------------------------------- /skin/pencil.svg: -------------------------------------------------------------------------------- 1 | 4 | 10 | 23 | 29 | -------------------------------------------------------------------------------- /skin/changelog.svg: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 44 | log 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /content/api/FiltaQuilla/schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "namespace": "FiltaQuilla", 4 | "functions": [ 5 | { 6 | "name": "saveFile", 7 | "type": "function", 8 | "async": true, 9 | "description": "saves a file.", 10 | "parameters": [ 11 | { 12 | "name": "file", 13 | "type": "object", 14 | "isInstanceOf": "File", 15 | "additionalProperties": true, 16 | "description": "a binary file object" 17 | }, 18 | { 19 | "name": "path", 20 | "type": "string", 21 | "description": "target path of file to save" 22 | }, 23 | { 24 | "name": "fileName", 25 | "type": "string", 26 | "optional": true, 27 | "description": "(optional) name to use for saved file, defaults to file.name" 28 | } 29 | ] 30 | }, 31 | { 32 | "name": "showAboutConfig", 33 | "type": "function", 34 | "async": false, 35 | "description": "opens about:config", 36 | "parameters": [ 37 | { 38 | "name": "filter", 39 | "type": "string", 40 | "description": "(optional) filter string to restrict configuration via search box (read only)" 41 | } 42 | ] 43 | } 44 | ] 45 | } 46 | ] -------------------------------------------------------------------------------- /html/fq-message.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FiltaQuilla NEWS 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

FiltaQuilla News

15 |
16 | 19 | 22 | 23 |
24 | inner message placeholder 25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /html/fq-util.js: -------------------------------------------------------------------------------- 1 | /* 2 | globals 3 | */ 4 | 5 | // var FiltaQuilla = FiltaQuilla || {}; 6 | console.log("Loading fq-util.js"); 7 | FiltaQuilla.Util = { 8 | getBaseURI: function (URL) { 9 | let hashPos = URL.indexOf("#"); 10 | let queryPos = URL.indexOf("?"); 11 | let baseURL = URL; 12 | 13 | if (hashPos > 0) { 14 | baseURL = URL.substring(0, hashPos); 15 | } else if (queryPos > 0) { 16 | baseURL = URL.substring(0, queryPos); 17 | } 18 | 19 | if (baseURL.endsWith("/")) { 20 | baseURL = baseURL.substring(0, baseURL.length - 1); 21 | } 22 | 23 | return baseURL; 24 | }, 25 | 26 | openHelpTab: async (fragment) => { 27 | let f = fragment ? `#${fragment}` : ""; 28 | let URL = `https://quickfilters.quickfolders.org/filtaquilla.html${f}`; 29 | await FiltaQuilla.Util.openLinkInTab(URL); 30 | }, 31 | 32 | openLinkInTab: async (URL) => { 33 | try { 34 | let baseURI = FiltaQuilla.Util.getBaseURI(URL); 35 | let tabs = await messenger.tabs.query({}); 36 | 37 | // Check if a FiltaQuilla help tab is already open 38 | let existingTab = tabs.find((t) => t.url && FiltaQuilla.Util.getBaseURI(t.url) === baseURI); 39 | 40 | if (existingTab) { 41 | await messenger.tabs.update(existingTab.id, { active: true, url: URL }); 42 | } else { 43 | await messenger.tabs.create({ url: URL }); 44 | } 45 | } catch (ex) { 46 | console.error("FiltaQuilla.Util.openLinkInTab() failed: ", ex); 47 | } 48 | }, 49 | 50 | getVersionSanitized: (Version) => { 51 | function strip(version, token) { 52 | let cutOff = version.indexOf(token); 53 | if (cutOff > 0) { 54 | // make sure to strip of any pre release labels 55 | return version.substring(0, cutOff); 56 | } 57 | return version; 58 | } 59 | 60 | 61 | let pureVersion = strip(Version, "pre"); 62 | pureVersion = strip(pureVersion, "beta"); 63 | pureVersion = strip(pureVersion, "alpha"); 64 | return strip(pureVersion, ".hc"); 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /release-notes.md: -------------------------------------------------------------------------------- 1 | **Version 6.0** 2 | 3 | **Latest News** 4 | 5 | We decided to concentrate on the latest versions from FiltaQuilla 140 onward because the code base was getting pretty unmanageable due to the substantial changes in Thunderbird Core and additional features added by the extended mail extension APIs. Being able to support this and legacy ways of doing the same thing within the same Add-on proved to be very difficult and produce unmanageable code. 6 | 7 | Even just supporting both ESR140 and the current release versions (currently 145), is already a difficult thing leading to substantially branching code paths within the same Add-on. This cut-off point will also be used to prepare for a number of long desired feature additions around saving attachments, which will be delivered next. 8 | 9 | Increased strict_min_version to 140.0. 10 | 11 | 12 | **Improvements** 13 | * Exclude signatures from saved / detached attachments [issue #372] 14 | * Made FiltaQuilla compatible with Thunderbird 146.*. 15 | * Removed legacy options screen and tightened UX in the modern html version for added clarity. [issue #379] 16 | 17 | **Bug Fixes** 18 | * Problems with saving some attachments - others work [issue #376] 19 | * Tb142 removed `messenger.detachAttachmentsWOPrompts` - reimplement detach attachments [issue #369] 20 | * Fixed: Double saving via the filter as pdf and additional txt file [issue #370] 21 | 22 | 23 | 24 | **TO DO NEXT** 25 | * Feature Request: Notification alert \[issue #240\]. 26 | * Support custom file names, including date, when saving / detaching attachments [issue #219] 27 | * Test attachRegEx_match and see if it needs updates for Tb128 / Release 28 | 29 | 30 | **Support My Work** 31 | As I am often asked about added features for filter conditions and actions for my Add-on [quickFilters](https://addons.thunderbird.net/addon/quickfilters/) - FiltaQuilla is a better location for extending Filter behavior - specifically adding new types of Actions and Conditions. If you want to **support the FiltaQuilla project**, please install quickFilters and **purchase a [quickFilters Pro](https://quickfilters.quickfolders.org/premium.html) license.** You can now also [donate directly here](https://quickfilters.quickfolders.org/filtaquilla.html#donate). -------------------------------------------------------------------------------- /content/i18n.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is provided by the addon-developer-support repository at 3 | * https://github.com/thundernest/addon-developer-support 4 | * 5 | * For usage descriptions, please check: 6 | * https://github.com/thundernest/addon-developer-support/tree/master/scripts/i18n 7 | * 8 | * Version: 1.1 9 | * 10 | * Derived from: 11 | * http://github.com/piroor/webextensions-lib-l10n 12 | * 13 | * Original license: 14 | * The MIT License, Copyright (c) 2016-2019 YUKI "Piro" Hiroshi 15 | * 16 | */ 17 | 18 | var i18n = { 19 | updateString(string) { 20 | let re = new RegExp(this.keyPrefix + "(.+?)__", "g"); 21 | return string.replace(re, (matched) => { 22 | const key = matched.slice(this.keyPrefix.length, -2); 23 | let rv = this.extension 24 | ? this.extension.localeData.localizeMessage(key) 25 | : messenger.i18n.getMessage(key); 26 | return rv || matched; 27 | }); 28 | }, 29 | 30 | updateSubtree(node) { 31 | const texts = document.evaluate( 32 | 'descendant::text()[contains(self::text(), "' + this.keyPrefix + '")]', 33 | node, 34 | null, 35 | XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, 36 | null 37 | ); 38 | for (let i = 0, maxi = texts.snapshotLength; i < maxi; i++) { 39 | const text = texts.snapshotItem(i); 40 | if (text.nodeValue.includes(this.keyPrefix)) 41 | text.nodeValue = this.updateString(text.nodeValue); 42 | } 43 | 44 | const attributes = document.evaluate( 45 | 'descendant::*/attribute::*[contains(., "' + this.keyPrefix + '")]', 46 | node, 47 | null, 48 | XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, 49 | null 50 | ); 51 | for (let i = 0, maxi = attributes.snapshotLength; i < maxi; i++) { 52 | const attribute = attributes.snapshotItem(i); 53 | if (attribute.value.includes(this.keyPrefix)) 54 | attribute.value = this.updateString(attribute.value); 55 | } 56 | }, 57 | 58 | updateDocument(options = {}) { 59 | this.extension = null; 60 | this.keyPrefix = "__MSG_"; 61 | if (options) { 62 | if (options.extension) this.extension = options.extension; 63 | if (options.keyPrefix) this.keyPrefix = options.keyPrefix; 64 | } 65 | this.updateSubtree(document); 66 | }, 67 | }; 68 | -------------------------------------------------------------------------------- /content/scripts/filtaquilla-folderProps.js: -------------------------------------------------------------------------------- 1 | 2 | /* replacement for folderPropsOverlay.js */ 3 | 4 | /* 5 | // Not permitted in Tb UI anymore 6 | // will be replaced with wx-compatible UI (folder context-menu) for future proofing 7 | 8 | function getBundleString(id, defaultText="no default text!", substitions = []) { 9 | var { ExtensionParent } = ChromeUtils.importESModule("resource://gre/modules/ExtensionParent.sys.mjs"); 10 | 11 | let extension = ExtensionParent.GlobalManager.getExtension("filtaquilla@mesquilla.com"); 12 | let localized = extension.localeData.localizeMessage(id, substitions); 13 | 14 | let s = ""; 15 | if (localized) { 16 | s = localized; 17 | } else { 18 | s = defaultText; 19 | console.warn(`Could not retrieve bundle string: ${id}`); 20 | } 21 | return s; 22 | } 23 | 24 | function onLoad(activatedWhileWindowOpen) { 25 | console.log(`Filtaquilla Folderprops\nonLoad(${activatedWhileWindowOpen})`); 26 | const folder = window.arguments[0].folder; 27 | // only inject this for IMAP folders: 28 | if (!folder) return; 29 | if (folder.incomingServerType != "imap") return; 30 | const Ci = Components.interfaces; 31 | if (Ci.nsMsgFolderFlags.Inbox & folder.flags) return; // no need to patch inbox! 32 | const previousCheck = document.querySelector("#folderCheckForNewMessages"); 33 | if (previousCheck) { 34 | const applyIncomingCb = document.createXULElement("checkbox"); 35 | applyIncomingCb.setAttribute("id", "filtaquilla-applyIncomingFilters"); 36 | applyIncomingCb.setAttribute("label", 37 | getBundleString("applyIncomingMails", "Run filters on incoming mails") 38 | ); 39 | applyIncomingCb.addEventListener("command", (event) => { 40 | const active = event.originalTarget.checked; 41 | console.log(`Change applyIncomingFilters of ${folder.prettyName || folder.localizedName} to: ${active}`); 42 | folder.setStringProperty("applyIncomingFilters", active ? "true" : ""); 43 | }); 44 | if (folder.getStringProperty("applyIncomingFilters")) { 45 | applyIncomingCb.setAttribute("checked", "true"); 46 | } 47 | previousCheck.parentNode.insertBefore(applyIncomingCb,previousCheck.nextSibling); 48 | } 49 | } 50 | 51 | function onUnload(isAddOnShutown) { 52 | console.log(`Filtaquilla Folderprops\nonUnload(${isAddOnShutown})`); 53 | } 54 | 55 | */ -------------------------------------------------------------------------------- /content/api/LegacyPrefs/README.md: -------------------------------------------------------------------------------- 1 | ## Objective 2 | 3 | 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. 4 | 5 | ## Usage 6 | 7 | Add the [LegacyPrefs API](https://github.com/thunderbird/addon-developer-support/tree/master/auxiliary-apis/LegacyPrefs) to your add-on. Your `manifest.json` needs an entry like this: 8 | 9 | ``` 10 | "experiment_apis": { 11 | "LegacyPrefs": { 12 | "schema": "api/LegacyPrefs/schema.json", 13 | "parent": { 14 | "scopes": ["addon_parent"], 15 | "paths": [["LegacyPrefs"]], 16 | "script": "api/LegacyPrefs/implementation.js" 17 | } 18 | } 19 | }, 20 | ``` 21 | 22 | ## API Functions 23 | 24 | This API provides the following functions: 25 | 26 | ### async getPref(aName, [aFallback]) 27 | 28 | 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``). 29 | 30 | ### async getUserPref(aName) 31 | 32 | 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``. 33 | 34 | ### clearUserPref(aName) 35 | 36 | Clears the user defined value for preference ``aName``. Subsequent calls to ``getUserPref(aName)`` will return ``null``. 37 | 38 | ### async setPref(aName, aValue) 39 | 40 | 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. 41 | 42 | ## API Events 43 | 44 | This API provides the following events: 45 | 46 | ### onChanged.addListener(listener, branch) 47 | 48 | Register a listener which is notified each time a value in the specified branch is changed. The listener returns the name and the new value of the changed preference. 49 | 50 | Example: 51 | 52 | ``` 53 | browser.LegacyPrefs.onChanged.addListener(async (name, value) => { 54 | console.log(`Changed value in "mailnews.": ${name} = ${value}`); 55 | }, "mailnews."); 56 | ``` 57 | 58 | --- 59 | 60 | 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/thunderbird/addon-developer-support/tree/master/scripts/preferences). 61 | -------------------------------------------------------------------------------- /skin/settings-simple.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 23 | 26 | 29 | 30 | -------------------------------------------------------------------------------- /skin/tooltip.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 34 | 47 | 55 | 62 | 63 | -------------------------------------------------------------------------------- /content/api/DomContentScript/implementation.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var { AppConstants } = ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs"); 4 | var DomContent_ESM = parseInt(AppConstants.MOZ_APP_VERSION, 10) >= 128; 5 | 6 | var { ExtensionCommon } = ChromeUtils.importESModule( 7 | "resource://gre/modules/ExtensionCommon.sys.mjs" 8 | ); 9 | 10 | var { ExtensionUtils } = DomContent_ESM 11 | ? ChromeUtils.importESModule("resource://gre/modules/ExtensionUtils.sys.mjs") 12 | : ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm"); 13 | 14 | var { ExtensionError } = ExtensionUtils; 15 | 16 | var registeredWindows = new Map(); 17 | 18 | var DomContentScript = class extends ExtensionCommon.ExtensionAPI { 19 | constructor(extension) { 20 | super(extension); 21 | 22 | this._windowListener = { 23 | // nsIWindowMediatorListener functions 24 | onOpenWindow(appWindow) { 25 | // A new window has opened. 26 | let domWindow = appWindow.docShell.domWindow; 27 | 28 | /** 29 | * Set up listeners to run the callbacks on the given window. 30 | * 31 | * @param aWindow {nsIDOMWindow} The window to set up. 32 | * @param aID {String} Optional. ID of the new caller that has registered right now. 33 | */ 34 | domWindow.addEventListener( 35 | "DOMContentLoaded", 36 | function() { 37 | // do stuff 38 | let windowChromeURL = domWindow.document.location.href; 39 | if (registeredWindows.has(windowChromeURL)) { 40 | let jsPath = registeredWindows.get(windowChromeURL); 41 | Services.scriptloader.loadSubScript(jsPath, domWindow, "UTF-8"); 42 | } 43 | }, 44 | { once: true } 45 | ); 46 | }, 47 | 48 | onCloseWindow(appWindow) { 49 | // One of the windows has closed. 50 | let domWindow = appWindow.docShell.domWindow; // we don't need to do anything (script only loads once) 51 | }, 52 | }; 53 | 54 | Services.wm.addListener(this._windowListener); 55 | 56 | } 57 | 58 | 59 | 60 | 61 | 62 | onShutdown(isAppShutdown) { 63 | if (isAppShutdown) { 64 | return; // the application gets unloaded anyway 65 | } 66 | Services.wm.removeListener(this._windowListener); 67 | } 68 | 69 | getAPI(context) { 70 | /** API IMPLEMENTATION **/ 71 | return { 72 | DomContentScript: { 73 | // only returns something, if a user pref value is set 74 | registerWindow: async function (windowUrl,jsPath) { 75 | registeredWindows.set(windowUrl,jsPath); 76 | } 77 | }, 78 | }; 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /skin/settings.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 23 | 26 | 29 | 30 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "FiltaQuilla", 4 | "description": "Mail filter custom actions and searches", 5 | "version": "6.0", 6 | "default_locale": "en", 7 | "author": "R Kent James, Axel Grude", 8 | "developer": { 9 | "name": "Axel Grude", 10 | "url": "https://quickfilters.quickfolders.org/filtaquilla.html" 11 | }, 12 | "applications": { 13 | "gecko": { 14 | "id": "filtaquilla@mesquilla.com", 15 | "strict_min_version": "140.0", 16 | "strict_max_version": "146.*" 17 | } 18 | }, 19 | "icons": { 20 | "32": "skin/filtaquilla-32.png", 21 | "64": "skin/filtaquilla-64.png" 22 | }, 23 | "background": { 24 | "page": "filtaquilla-background.html" 25 | }, 26 | "permissions": ["messagesRead", "messagesModifyPermanent", "menus", "notifications", "storage", "tabs"], 27 | "browser_action": { 28 | "default_icon": { 29 | "32": "skin/filtaquilla-32.png", 30 | "64": "skin/filtaquilla-64.png" 31 | }, 32 | "default_windows": ["normal"], 33 | "type": "menu", 34 | "allowed_spaces": ["mail", "settings"] 35 | }, 36 | "experiment_apis": { 37 | "FiltaQuilla": { 38 | "schema": "content/api/FiltaQuilla/schema.json", 39 | "parent": { 40 | "scopes": ["addon_parent"], 41 | "paths": [["FiltaQuilla"]], 42 | "script": "content/api/FiltaQuilla/implementation.js" 43 | } 44 | }, 45 | "WindowListener": { 46 | "schema": "content/api/WindowListener/schema.json", 47 | "parent": { 48 | "scopes": ["addon_parent"], 49 | "paths": [["WindowListener"]], 50 | "script": "content/api/WindowListener/implementation.js" 51 | } 52 | }, 53 | "NotifyTools": { 54 | "schema": "content/api/NotifyTools/schema.json", 55 | "parent": { 56 | "scopes": ["addon_parent"], 57 | "paths": [["NotifyTools"]], 58 | "script": "content/api/NotifyTools/implementation.js", 59 | "events": ["startup"] 60 | } 61 | }, 62 | "LegacyPrefs": { 63 | "schema": "content/api/LegacyPrefs/schema.json", 64 | "parent": { 65 | "scopes": ["addon_parent"], 66 | "paths": [["LegacyPrefs"]], 67 | "script": "content/api/LegacyPrefs/implementation.js" 68 | } 69 | }, 70 | "DomContentScript": { 71 | "schema": "content/api/DomContentScript/schema.json", 72 | "parent": { 73 | "scopes": ["addon_parent"], 74 | "paths": [["DomContentScript"]], 75 | "script": "content/api/DomContentScript/implementation.js" 76 | } 77 | } 78 | }, 79 | "options_ui": { 80 | "page": "html/fq-settings.html", 81 | "open_in_tab": true 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /skin/filtaquilla.css: -------------------------------------------------------------------------------- 1 | 2 | .ruleactiontarget[type="filtaquilla@mesquilla.com#launchFile"], 3 | .ruleactiontarget[type="filtaquilla@mesquilla.com#runFile"], 4 | .ruleactiontarget[type="filtaquilla@mesquilla.com#addSender"], 5 | .ruleactiontarget[type="filtaquilla@mesquilla.com#saveAttachment"] , 6 | .ruleactiontarget[type="filtaquilla@mesquilla.com#detachAttachments"] , 7 | .ruleactiontarget[type="filtaquilla@mesquilla.com#javascriptAction"] , 8 | .ruleactiontarget[type="filtaquilla@mesquilla.com#javascriptActionBody"] , 9 | .ruleactiontarget[type="filtaquilla@mesquilla.com#saveMessageAsFile"] { 10 | flex-grow: 2; 11 | /* -moz-box-flex: 1; */ 12 | } 13 | 14 | filtaquilla-ruleactiontarget-runpicker toolbarbutton, 15 | filtaquilla-ruleactiontarget-launchpicker toolbarbutton, 16 | filtaquilla-ruleactiontarget-directorypicker toolbarbutton, 17 | filtaquilla-ruleactiontarget-tonequillapicker toolbarbutton, 18 | filtaquilla-ruleactiontarget-templatepicker toolbarbutton { 19 | -moz-context-properties: fill; 20 | fill: currentColor; 21 | } 22 | 23 | toolbarbutton.focusbutton { 24 | -moz-user-focus: normal; 25 | } 26 | 27 | /* style the file picker + textbox elements so that the textbox stretches */ 28 | hbox.flexelementcontainer { 29 | display: inline-flex !important; 30 | width: -moz-available; 31 | } 32 | hbox.flexelementcontainer[hidden] { 33 | display: none !important; 34 | } 35 | 36 | input.flexinput, 37 | menulist.flexinput { 38 | flex-grow: 2 !important; 39 | } 40 | 41 | 42 | .ruleactionitem.filtaquillaAB menuitem { 43 | list-style-image: url("chrome://messenger/skin/icons/address.svg"); 44 | opacity: 0.75 !important; 45 | } 46 | 47 | .ruleactionitem.filtaquillaAB menuitem.mailing-list { 48 | list-style-image: url("chrome://messenger/skin/icons/ablist.svg"); 49 | opacity: 1.0; 50 | } 51 | 52 | .ruleactionitem.filtaquillaAB menuitem.is-remote { 53 | list-style-image: url("chrome://messenger/skin/icons/globe.svg"); 54 | opacity: 1.0; 55 | } 56 | 57 | .ruleactionitem.filtaquillaAB menuitem.is-remote.is-secure { 58 | list-style-image: url("chrome://messenger/skin/icons/globe-secure.svg"); 59 | opacity: 1.0; 60 | } 61 | 62 | /* textboxes */ 63 | .search-value-custom[searchAttribute="filtaquilla@mesquilla.com#attachmentRegex"], 64 | .search-value-custom[searchAttribute="filtaquilla@mesquilla.com#headerRegex"], 65 | .search-value-custom[searchAttribute="filtaquilla@mesquilla.com#searchBcc"], 66 | .search-value-custom[searchAttribute="filtaquilla@mesquilla.com#folderName"], 67 | .search-value-custom[searchAttribute="filtaquilla@mesquilla.com#subjectRegex"], 68 | .search-value-custom[searchAttribute="filtaquilla@mesquilla.com#threadheadtag"], 69 | .search-value-custom[searchAttribute="filtaquilla@mesquilla.com#threadanytag"] { 70 | /* -moz-box-flex: 1; */ 71 | flex-grow: 2; 72 | } 73 | 74 | .filtaquilla_help { 75 | list-style-image: url("resource://filtaquilla-skin/help.svg"); 76 | } 77 | .filtaquilla_build { 78 | list-style-image: url("resource://filtaquilla-skin/pencil.svg"); 79 | } 80 | .filtaquilla_play { 81 | list-style-image: url("resource://filtaquilla-skin/media-play.svg"); 82 | } 83 | 84 | /* push down the text */ 85 | .filtaquilla_topinput { 86 | align-items: end; 87 | } -------------------------------------------------------------------------------- /release-notes.html: -------------------------------------------------------------------------------- 1 | We decided to concentrate on the latest versions from FIltaQuilla v140 onward because the code base was getting pretty unmanageable due to the substantial changes in Thunderbird Core and additional features added by the extended mail extension APIs. Being able to support this and legacy ways of doing the same thing within the same Add-on proved to be very difficult and produce unmanageable code. 2 | 3 | Even just supporting both ESR140 and the current release versions (currently 145), is already a difficult thing leading to substantially branching code paths within the same Add-on. This cut-off point will also be used to prepare for a number of long desired feature additions around saving attachments, which will be delivered next. 4 | 5 | Increased strict_min_version to 140.0. 6 | 7 | If you would like to support FiltaQuilla with donation you can now do so here. For more ways of supporting me work, please read the section below after the change log. 8 | 9 | 10 | Improvements 11 | 16 | Bug Fixes 17 | 22 | Planned Next 23 | 28 | 29 | Support My Work 30 | 31 | As I am often asked about added features for filter conditions and actions for my Add-on quickFilters - FiltaQuilla is a better location for extending Filter behavior - specifically adding new types of Actions and Conditions. 32 | 33 | If you want to support the FiltaQuilla project, please install quickFilters and 34 | purchase a quickFilters Pro license. 35 | 36 | You can now also donate directly here. 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /skin/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | -------------------------------------------------------------------------------- /content/jsEditor.js: -------------------------------------------------------------------------------- 1 | /* 2 | ***** BEGIN LICENSE BLOCK ***** 3 | * This file is part of FiltaQuilla, Custom Filter Actions, by Mesquilla. 4 | * 5 | * FiltaQuilla is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * You should have received a copy of the GNU General Public License 11 | * along with FiltaQuilla. If not, see . 12 | * 13 | * Software distributed under the License is distributed on an "AS IS" basis, 14 | * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 15 | * for the specific language governing rights and limitations under the 16 | * License. 17 | * 18 | * The Original Code is FiltaQuilla code. 19 | * 20 | * The Initial Developer of the Original Code is 21 | * Kent James 22 | * Portions created by the Initial Developer are Copyright (C) 2008 23 | * the Initial Developer. All Rights Reserved. 24 | * 25 | * Contributor(s): 26 | * 27 | * ***** END LICENSE BLOCK ***** 28 | */ 29 | 30 | 31 | // The unicode line separator \u2028 is recognized by js as a line terminator, 32 | // but survives the storage in a filter editor file without getting 33 | // truncated. So we use it to store the newlines. 34 | const LS = "\u2028"; // this is used to encode linebreaks. 35 | 36 | function encodeScript(script) { 37 | // Re-encode (newlines to LS) before returning 38 | return script.replace(/\n/g, LS); 39 | } 40 | 41 | function decodeScript(script) { 42 | // Replace the line separator (LS) with actual newlines 43 | return script.replace(new RegExp(LS, "g"), "\n"); 44 | } 45 | 46 | // Function to send the updated script to the background page 47 | function sendUpdatedScript(textarea) { 48 | const rawScript = textarea.value; 49 | const encodedScript = encodeScript(rawScript); // Encode if needed: replace line breaks! 50 | 51 | // Send the updated script back to the background page 52 | messenger.runtime.sendMessage({ 53 | command: "updateActionScript", 54 | script: encodedScript, 55 | }); 56 | } 57 | 58 | document.addEventListener("DOMContentLoaded", async () => { 59 | const textarea = document.getElementById("jscode"); 60 | const accept = document.getElementById("accept"); 61 | const cancel = document.getElementById("cancel"); 62 | 63 | accept.addEventListener("click", () => { 64 | sendUpdatedScript(textarea); 65 | window.close(); 66 | }); 67 | 68 | document.getElementById("cancel").addEventListener("click", () => { 69 | window.close(); 70 | }); 71 | 72 | const manifest = await messenger.runtime.getManifest(), 73 | addonName = manifest.name; 74 | document.getElementById("pageHead").textContent = messenger.i18n.getMessage( 75 | "filtaquilla.editJavascript", 76 | addonName 77 | ); 78 | cancel.textContent = messenger.i18n.getMessage("regex.cancel", addonName); 79 | }); 80 | 81 | browser.runtime.onMessage.addListener(function (request, sender, sendResponse) { 82 | if (request.action === "initActionScript") { 83 | // Decode stored string (replace LS with real newlines) 84 | // textarea.value = rawString.replace(new RegExp(LS, "g"), "\n"); 85 | 86 | const script = decodeScript(request.script); 87 | document.getElementById("jscode").value = script || ""; // Set the initial script value 88 | } 89 | }); 90 | 91 | 92 | -------------------------------------------------------------------------------- /skin/help-tip.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 18 | 20 | 38 | 52 | 57 | 62 | 67 | 72 | 73 | -------------------------------------------------------------------------------- /content/api/WindowListener/schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "namespace": "WindowListener", 4 | "functions": [ 5 | { 6 | "name": "registerDefaultPrefs", 7 | "type": "function", 8 | "parameters": [ 9 | { 10 | "name": "aPath", 11 | "type": "string", 12 | "description": "Relative path to the default file." 13 | } 14 | ] 15 | }, 16 | { 17 | "name": "registerOptionsPage", 18 | "type": "function", 19 | "parameters": [ 20 | { 21 | "name": "aPath", 22 | "type": "string", 23 | "description": "Path to the options page, which should be made accessible in the (legacy) Add-On Options menu." 24 | } 25 | ] 26 | }, 27 | { 28 | "name": "registerChromeUrl", 29 | "type": "function", 30 | "description": "Register folders which should be available as chrome:// urls (as defined in the legacy chrome.manifest)", 31 | "parameters": [ 32 | { 33 | "name": "data", 34 | "type": "array", 35 | "items": { 36 | "type": "array", 37 | "items" : { 38 | "type": "string" 39 | } 40 | }, 41 | "description": "Array of manifest url definitions (content, locale, resource)" 42 | } 43 | ] 44 | }, 45 | { 46 | "name": "waitForMasterPassword", 47 | "type": "function", 48 | "async": true, 49 | "parameters": [] 50 | }, 51 | { 52 | "name": "openOptionsDialog", 53 | "type": "function", 54 | "parameters": [ 55 | { 56 | "name": "windowId", 57 | "type": "integer", 58 | "description": "Id of the window the dialog should be opened from." 59 | } 60 | ] 61 | }, 62 | { 63 | "name": "startListening", 64 | "type": "function", 65 | "async": true, 66 | "parameters": [] 67 | }, 68 | { 69 | "name": "registerWindow", 70 | "type": "function", 71 | "parameters": [ 72 | { 73 | "name": "windowHref", 74 | "type": "string", 75 | "description": "Url of the window, which should be listen for." 76 | }, 77 | { 78 | "name": "jsFile", 79 | "type": "string", 80 | "description": "Path to the JavaScript file, which should be loaded into the window." 81 | } 82 | ] 83 | }, 84 | { 85 | "name": "registerStartupScript", 86 | "type": "function", 87 | "parameters": [ 88 | { 89 | "name": "aPath", 90 | "type": "string", 91 | "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." 92 | } 93 | ] 94 | }, 95 | { 96 | "name": "registerShutdownScript", 97 | "type": "function", 98 | "parameters": [ 99 | { 100 | "name": "aPath", 101 | "type": "string", 102 | "description": "Path to a JavaScript file, which should be executed on add-on shutdown." 103 | } 104 | ] 105 | } 106 | ] 107 | } 108 | ] 109 | -------------------------------------------------------------------------------- /content/folderPropsOverlay.js: -------------------------------------------------------------------------------- 1 | /* 2 | ***** BEGIN LICENSE BLOCK ***** 3 | * This file is part of filtaquilla by Mesquilla. 4 | * 5 | * This is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * You should have received a copy of the GNU General Public License 11 | * along with this. If not, see . 12 | * 13 | * Software distributed under the License is distributed on an "AS IS" basis, 14 | * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 15 | * for the specific language governing rights and limitations under the 16 | * License. 17 | * 18 | * The Original Code is Mesquilla code. 19 | * 20 | * The Initial Developer of the Original Code is 21 | * Kent James 22 | * Portions created by the Initial Developer are Copyright (C) 2008 23 | * the Initial Developer. All Rights Reserved. 24 | * 25 | * Contributor(s): 26 | * 27 | * ***** END LICENSE BLOCK ***** 28 | */ 29 | 30 | // folder properties overlay. Unfortunately there are not adequate ids in the 31 | // filter properties xul to make a normal overlay possible, so instead we have 32 | // to add our xul dynamically. 33 | // will be replaced with wx-compatible UI (folder context-menu) for future proofing 34 | 35 | /* 36 | 37 | var { InheritedPropertiesGrid } = ChromeUtils.importESModule( 38 | "resource://filtaquilla/inheritedPropertiesGrid.sys.mjs" 39 | ); 40 | 41 | (function() { 42 | // global scope variables 43 | this.filtaquillaFolderProps = {}; 44 | 45 | // local shorthand for the global reference 46 | let self = this.filtaquillaFolderProps; 47 | 48 | // module-level variables 49 | const Cc = Components.classes; 50 | const Ci = Components.interfaces; 51 | const Cu = Components.utils; 52 | 53 | let folder; // nsIMsgFolder passed to the window 54 | 55 | self.onLoad = function onLoad(e) { 56 | folder = window.arguments[0].folder; 57 | 58 | // setup UI for the "applyIncomingFilters" inherited property, but only for 59 | // imap non-inbox folders. 60 | if ( !(folder instanceof Ci.nsIMsgImapMailFolder) || 61 | (folder.getFlag(Ci.nsMsgFolderFlags.Inbox)) || 62 | (folder.getFlag(Ci.nsMsgFolderFlags.Virtual)) ) 63 | return; 64 | 65 | window.gInheritTarget = folder; 66 | 67 | // create or get the rows from the inherit grid 68 | let rows = InheritedPropertiesGrid.getInheritRows(document), 69 | row; 70 | try { 71 | row = InheritedPropertiesGrid.createInheritRow("applyIncomingFilters", folder, document); 72 | } catch (e) { 73 | Cu.reportError(e); 74 | } 75 | 76 | if (row) { 77 | rows.appendChild(row); 78 | // extend the ondialogaccept attribute 79 | let dialog = document.getElementsByTagName("dialog")[0]; 80 | dialog.setAttribute("ondialogaccept", "filtaquillaFolderProps.onAcceptInherit();" + 81 | dialog.getAttribute("ondialogaccept")); 82 | } 83 | else 84 | throw "row not created for property applyIncomingFilters"; 85 | }, 86 | 87 | self.onAcceptInherit = function applyIncomingFiltersOnAcceptInherit() 88 | { 89 | InheritedPropertiesGrid.onAcceptInherit("applyIncomingFilters", folder, document); 90 | } 91 | 92 | })(); 93 | 94 | window.addEventListener("load", function(e) { filtaquillaFolderProps.onLoad(e); }, false); 95 | 96 | */ -------------------------------------------------------------------------------- /skin/new.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 37 | 39 | 42 | 46 | 50 | 51 | 57 | 67 | 68 | 73 | 78 | 87 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /defaults/preferences/filtaquilla.js: -------------------------------------------------------------------------------- 1 | /* 2 | globals 3 | pref 4 | */ 5 | 6 | // See http://kb.mozillazine.org/Localize_extension_descriptions 7 | pref("extensions.filtaquilla@mesquilla.com.description", "chrome://filtaquilla/locale/filtaquilla.properties"); 8 | // the maximum number of items to scan in a thread search 9 | pref("extensions.filtaquilla.maxthreadscan", 20); 10 | // filter actions 11 | pref("extensions.filtaquilla.attachmentTimeoutMs", 25000); // sync attachment saving. timeout per email 12 | // pref("extensions.filtaquilla.fixEncodingInFileNames", true); // [bug 1992976] 13 | pref("extensions.filtaquilla.subjectAppend.enabled", false); 14 | pref("extensions.filtaquilla.subjectSuffix.enabled", false); 15 | pref("extensions.filtaquilla.removeKeyword.enabled", false); 16 | pref("extensions.filtaquilla.removeFlagged.enabled", false); 17 | pref("extensions.filtaquilla.markUnread.enabled", false); 18 | pref("extensions.filtaquilla.markReplied.enabled", false); 19 | pref("extensions.filtaquilla.noBiff.enabled", false); 20 | pref("extensions.filtaquilla.copyAsRead.enabled", false); 21 | pref("extensions.filtaquilla.lastUpdateMessage", "0"); 22 | pref("extensions.filtaquilla.launchFile.enabled", true); 23 | pref("extensions.filtaquilla.runFile.enabled", false); 24 | pref("extensions.filtaquilla.runFile.unicode", false); // [issue 102] 25 | pref("extensions.filtaquilla.trainAsJunk.enabled", false); 26 | pref("extensions.filtaquilla.trainAsGood.enabled", false); 27 | pref("extensions.filtaquilla.print.enabled", true); 28 | pref("extensions.filtaquilla.print.enablePrintToolsNG", false); 29 | pref("extensions.filtaquilla.print.allowDuplicates", false); 30 | pref("extensions.filtaquilla.print.delay", 10); // test 31 | pref("extensions.filtaquilla.addSender.enabled", false); 32 | pref("extensions.filtaquilla.saveAttachment.enabled", false); 33 | pref("extensions.filtaquilla.detachAttachments.enabled", false); 34 | pref("extensions.filtaquilla.fileNames.spaceCharacter", " "); 35 | pref("extensions.filtaquilla.fileNames.maxLength", 60); 36 | pref("extensions.filtaquilla.fileNames.whiteList", ""); 37 | pref("extensions.filtaquilla.javascriptAction.enabled", false); 38 | pref("extensions.filtaquilla.javascriptActionBody.enabled", false); 39 | pref("extensions.filtaquilla.tonequilla.enabled", false); 40 | pref("extensions.filtaquilla.tonequilla.soundDelay", 100); 41 | pref("extensions.filtaquilla.tonequilla.fadeOut", 25); 42 | pref("extensions.filtaquilla.saveMessageAsFile.enabled", false); 43 | pref("extensions.filtaquilla.moveLater.enabled", false); 44 | pref("extensions.filtaquilla.regexpCaseInsensitive.enabled", true); 45 | pref("extensions.filtaquilla.regexpHeader.addressMultiLine", false); // [issue 329] 46 | pref("extensions.filtaquilla.archiveMessage.enabled", false); 47 | pref("extensions.filtaquilla.smarttemplates.fwd.enabled", false); 48 | pref("extensions.filtaquilla.smarttemplates.rsp.enabled", false); 49 | pref("extensions.filtaquilla.lastSelectedOptionsTab", "actions"); // actions, conditions, support 50 | // search terms 51 | pref("extensions.filtaquilla.SubjectRegexEnabled", true); 52 | pref("extensions.filtaquilla.BodyRegexEnabled", true); 53 | pref("extensions.filtaquilla.SubjectBodyRegexEnabled", false); 54 | pref("extensions.filtaquilla.HeaderRegexEnabled", false); 55 | pref("extensions.filtaquilla.JavascriptEnabled", false); 56 | pref("extensions.filtaquilla.SearchBccEnabled", true); 57 | pref("extensions.filtaquilla.FolderNameEnabled", false); 58 | pref("extensions.filtaquilla.ThreadHeadTagEnabled", false); 59 | pref("extensions.filtaquilla.ThreadAnyTagEnabled", false); 60 | 61 | // debug 62 | pref("extensions.filtaquilla.debug", false); 63 | pref("extensions.filtaquilla.debug.attachments", false); 64 | pref("extensions.filtaquilla.debug.firstrun", false); 65 | pref("extensions.filtaquilla.debug.notifications", false); 66 | pref("extensions.filtaquilla.debug.PrintingToolsNG", false); 67 | pref("extensions.filtaquilla.debug.SmartTemplates", false); 68 | pref("extensions.filtaquilla.debug.regexSubject", false); 69 | pref("extensions.filtaquilla.debug.regexHeader", false); 70 | pref("extensions.filtaquilla.debug.regexBody", false); 71 | pref("extensions.filtaquilla.debug.regexBody_parts", false); 72 | pref("extensions.filtaquilla.debug.mimeBody", false); 73 | pref("extensions.filtaquilla.debug.isLocal", false); 74 | pref("extensions.filtaquilla.debug.sounds", false); 75 | 76 | 77 | // upgrade handling stuff 78 | pref("extensions.filtaquilla.installDate", ""); 79 | pref("extensions.filtaquilla.firstRun", true); 80 | pref("extensions.filtaquilla.version", "?"); 81 | 82 | // vim: set expandtab tabstop=2 shiftwidth=2 83 | -------------------------------------------------------------------------------- /content/api/LegacyPrefs/schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "namespace": "LegacyPrefs", 4 | "events": [ 5 | { 6 | "name": "onChanged", 7 | "type": "function", 8 | "description": "Fired when a preference has been changed.", 9 | "parameters": [ 10 | { 11 | "name": "name", 12 | "type": "string", 13 | "description": "Name of the preference." 14 | }, 15 | { 16 | "name": "value", 17 | "type": "any", 18 | "description": "Value of the preference." 19 | } 20 | ], 21 | "extraParameters": [ 22 | { 23 | "name": "branch", 24 | "description": "The branch to observe.", 25 | "type": "string" 26 | } 27 | ] 28 | } 29 | ], 30 | "functions": [ 31 | { 32 | "name": "getUserPref", 33 | "type": "function", 34 | "async": true, 35 | "description": "Gets a user value from the legacy pref system.", 36 | "parameters": [ 37 | { 38 | "name": "aName", 39 | "type": "string", 40 | "description": "Name of the preference." 41 | } 42 | ] 43 | }, 44 | { 45 | "name": "getPref", 46 | "type": "function", 47 | "async": true, 48 | "description": "Gets a value from the legacy pref system.", 49 | "parameters": [ 50 | { 51 | "name": "aName", 52 | "type": "string", 53 | "description": "Name of the preference." 54 | }, 55 | { 56 | "name": "aFallback", 57 | "type": "any", 58 | "description": "Value to be returned, if the requested preference does not exist.", 59 | "optional": true, 60 | "default": null 61 | } 62 | ] 63 | }, 64 | { 65 | "name": "createPref", 66 | "type": "function", 67 | "async": true, 68 | "description": "Creates a new entry in the legacy pref system.", 69 | "parameters": [ 70 | { 71 | "name": "aName", 72 | "type": "string", 73 | "description": "Name of the preference." 74 | }, 75 | { 76 | "name": "aValue", 77 | "choices": [ 78 | { 79 | "type": "string" 80 | }, 81 | { 82 | "type": "integer" 83 | }, 84 | { 85 | "type": "boolean" 86 | } 87 | ], 88 | "description": "Value to be set." 89 | } 90 | ] 91 | }, 92 | { 93 | "name": "setPref", 94 | "type": "function", 95 | "async": true, 96 | "description": "Sets a value for an existing pref of the legacy pref system.", 97 | "parameters": [ 98 | { 99 | "name": "aName", 100 | "type": "string", 101 | "description": "Name of the preference." 102 | }, 103 | { 104 | "name": "aValue", 105 | "choices": [ 106 | { 107 | "type": "string" 108 | }, 109 | { 110 | "type": "integer" 111 | }, 112 | { 113 | "type": "boolean" 114 | } 115 | ], 116 | "description": "Value to be set." 117 | } 118 | ] 119 | }, 120 | { 121 | "name": "setDefaultPref", 122 | "type": "function", 123 | "async": true, 124 | "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.", 125 | "parameters": [ 126 | { 127 | "name": "aName", 128 | "type": "string", 129 | "description": "Name of the preference." 130 | }, 131 | { 132 | "name": "aValue", 133 | "choices": [ 134 | { 135 | "type": "string" 136 | }, 137 | { 138 | "type": "integer" 139 | }, 140 | { 141 | "type": "boolean" 142 | } 143 | ], 144 | "description": "Default value to be set." 145 | } 146 | ] 147 | }, 148 | { 149 | "name": "clearUserPref", 150 | "type": "function", 151 | "description": "Removes a user value from the legacy pref system.", 152 | "parameters": [ 153 | { 154 | "name": "aName", 155 | "type": "string", 156 | "description": "Name of the preference." 157 | } 158 | ] 159 | } 160 | ] 161 | } 162 | ] 163 | -------------------------------------------------------------------------------- /html/fq-message.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --canvas: rgb(245,245,245); 3 | --canvastext: rgb(15,15,15); 4 | --commandbar: rgba(10,10,10,0.1) 5 | --actionbtn-backcolor: darkred; 6 | --actionbtn-color: white; 7 | color-scheme: light dark; 8 | } 9 | @media (prefers-color-scheme: dark) { 10 | :root { 11 | --canvas: rgb(15,15,15); 12 | --canvastext:rgb(245,245,245); 13 | --commandbar: rgba(250,250,250,0.1) 14 | } 15 | } 16 | 17 | body { 18 | background-color: var(--canvas); 19 | color: var(--canvastext); 20 | } 21 | 22 | /** STYLES FOR THE MODELESS message window smarteTemplate-msg.xul ****/ 23 | #titleBox { 24 | background-position: center; 25 | background-repeat: no-repeat; 26 | font-size: 1.8em; 27 | text-align: center; 28 | background-color: transparent; 29 | } 30 | 31 | #messageCanvas { 32 | background-position: top 1em right 2em ; 33 | background-repeat: no-repeat; 34 | background-image: url("../skin/filtaquilla-64.png"); 35 | background-size: 128px 128px; 36 | border: 1px solid rgba(64, 2, 2, 0.1); /* test */ 37 | border-radius: 0.3rem; 38 | box-sizing: border-box; 39 | min-width: 200px; 40 | min-height: 200px; 41 | width: 100%; 42 | } 43 | 44 | .bottomhint { 45 | text-align: center; 46 | } 47 | 48 | 49 | #commands { 50 | display: flex; 51 | justify-content: center; /* aligns buttons left, change as needed */ 52 | gap: 2em; /* spacing between buttons */ 53 | padding: 1em 0; 54 | } 55 | 56 | #commands button { 57 | box-shadow: 3px 3px 2px rgba(50,0,0,0.1); 58 | background-color: var(--actionbtn-backcolor,darkred); 59 | color: var(--actionbtn-color,white); 60 | border: none; 61 | padding: 8px 16px; 62 | font-size: 12pt; 63 | cursor: pointer; 64 | border-radius: 4px; 65 | } 66 | 67 | #commands button:hover { 68 | background-image: linear-gradient(to bottom, #6d0019 0%, #8f0222 34%, #e20425 100%); 69 | } 70 | 71 | #commands button:active { 72 | background-image: linear-gradient(to bottom, #633b00 0%, #750202 34%, #ef8204 100%); 73 | } 74 | 75 | /* remove default buttons, we add our own */ 76 | .dialog-button-box { 77 | display: none; 78 | } 79 | 80 | #ok, #cancel, #yes, #no, #countDown { 81 | font-size: 12pt; 82 | } 83 | 84 | body { 85 | background-color: var(--canvas) !important; 86 | color: var(--canvastext) !important; 87 | } 88 | 89 | .wrapper { 90 | font-size: 12pt; 91 | margin: 0 3em 1.5em; 92 | min-width: 300px; 93 | width: 80%; 94 | } 95 | 96 | #innerMessage { 97 | height: auto; 98 | flex-direction: column; 99 | } 100 | 101 | #innerMessage p, #innerMessage br { 102 | display: block; 103 | line-height: 1.2em; 104 | } 105 | #innerMessage p { 106 | /*! text-align: justify; */ 107 | /* color: rgb(2,2,2) !important; */ /* leave text color to theme! */ 108 | margin-bottom: 0.6em; 109 | max-width: 850px; 110 | } 111 | 112 | #innerMessage #code { 113 | background-color: #FFFFFF; 114 | box-shadow: inset 0 0 5px rgba(0,0,0,0.5); 115 | color: rgb(40,40,40); 116 | font-size: 10pt; 117 | font-family: Consolas,monaco,monospace !important; 118 | min-height: 50px; 119 | max-height: 250px !important; 120 | overflow-y: scroll; 121 | } 122 | 123 | #innerMessage #code p { 124 | margin-top: 2px; 125 | margin-bottom: 2px; 126 | } 127 | 128 | #messageCanvas { 129 | height: 100%; 130 | display: flex; 131 | flex-direction: column; 132 | overflow: hidden; 133 | } 134 | 135 | /* overwrite rules from popup.css */ 136 | body *:not(button) { 137 | background-color: unset !important; 138 | color: unset !important; 139 | } 140 | body p, #innerMessage { 141 | background-color: transparent !important; 142 | } 143 | 144 | .messageContents { 145 | background-color: transparent; 146 | flex-grow: 1; 147 | overflow-y: auto; 148 | } 149 | 150 | html, body { 151 | height: 100%; 152 | margin: 0; 153 | padding: 0; 154 | } 155 | 156 | #commands { 157 | position: sticky; 158 | bottom: 0; 159 | background: var(--commandbar,rgba(250,250,250,0.1)); 160 | padding: 0.5rem; 161 | border-top: 1px solid #ccc; 162 | z-index: 10; 163 | } 164 | 165 | #commands button.actionlinks { 166 | margin-top: 0; 167 | } 168 | 169 | /* use this for pringin html code */ 170 | pre { 171 | font-size: 0.85rem; 172 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 173 | white-space: pre-wrap; 174 | max-width: 100%; 175 | overflow-wrap: break-word; 176 | } 177 | 178 | 179 | .announcement { 180 | color: white !important; 181 | background-color: rgba(6, 6, 112, 0.9); 182 | background-image: linear-gradient(55deg, rgba(6, 6, 112, 0.8) 0%, rgba(115, 3, 85, 0.8) 100%); 183 | border-radius: 0.5em; 184 | padding-block: 0.6em !important; 185 | margin-bottom: 1.2em; 186 | padding: 0.1em 1em; 187 | } 188 | 189 | .link-visited { 190 | color: #d2691e !important; 191 | } 192 | span.link-visited { 193 | font-weight: bold; 194 | font-size: 0.8em; 195 | } 196 | 197 | [hidden] { 198 | display: none !important; 199 | } -------------------------------------------------------------------------------- /content/api/WindowListener/changelog.md: -------------------------------------------------------------------------------- 1 | Version: 1.62 2 | ------------- 3 | - fix bug in fullyLoaded() 4 | 5 | Version: 1.61 6 | ------------- 7 | - adjusted to Thunderbird Supernova (Services is now in globalThis) 8 | 9 | Version: 1.60 10 | ------------- 11 | - explicitly set hasAddonManagerEventListeners flag to false on uninstall 12 | 13 | Version: 1.59 14 | ------------- 15 | - store hasAddonManagerEventListeners flag in add-on scope instead on the global 16 | window again, and clear it upon add-on removal 17 | 18 | Version: 1.58 19 | ------------- 20 | - hard fork WindowListener v1.57 implementation and continue to serve it for 21 | Thunderbird 111 and older 22 | - WindowListener v1.58 supports injection into nested browsers of the new 23 | mailTab front end of Thunderbird Supernova and allows "about:message" and 24 | "about:3pane" to be valid injection targets. More information can be found here: 25 | https://developer.thunderbird.net/thunderbird-development/codebase-overview/mail-front-end 26 | 27 | Version: 1.57 28 | ------------- 29 | - fix race condition which could prevent the AOM tab to be monkey patched correctly 30 | 31 | Version: 1.56 32 | ------------- 33 | - be precise on which revision the wrench symbol should be displayed, instead of 34 | the options button 35 | 36 | Version: 1.54 37 | ------------- 38 | - fix "ownerDoc.getElementById() is undefined" bug 39 | 40 | Version: 1.53 41 | ------------- 42 | - fix "tab.browser is undefined" bug 43 | 44 | Version: 1.52 45 | ------------- 46 | - clear cache only if add-on is uninstalled/updated, not on app shutdown 47 | 48 | Version: 1.51 49 | ------------- 50 | - use wrench button for options for TB78.10 51 | 52 | Version: 1.50 53 | ------------- 54 | - use built-in CSS rules to fix options button for dark themes (thanks to Thunder) 55 | - fix some occasions where options button was not added 56 | 57 | Version: 1.49 58 | ------------- 59 | - fixed missing eventListener for Beta + Daily 60 | 61 | Version: 1.48 62 | ------------- 63 | - moved notifyTools into its own NotifyTools API. 64 | 65 | Version: 1.39 66 | ------------- 67 | - fix for 68 68 | 69 | Version: 1.36 70 | ------------- 71 | - fix for beta 87 72 | 73 | Version: 1.35 74 | ------------- 75 | - add support for options button/menu in add-on manager and fix 68 double menu entry 76 | 77 | Version: 1.34 78 | ------------- 79 | - fix error in unload 80 | 81 | Version: 1.33 82 | ------------- 83 | - fix for e10s 84 | 85 | Version: 1.30 86 | ------------- 87 | - replace setCharPref by setStringPref to cope with UTF-8 encoding 88 | 89 | Version: 1.29 90 | ------------- 91 | - open options window centered 92 | 93 | Version: 1.28 94 | ------------- 95 | - do not crash on missing icon 96 | 97 | Version: 1.27 98 | ------------- 99 | - add openOptionsDialog() 100 | 101 | Version: 1.26 102 | ------------- 103 | - pass WL object to legacy preference window 104 | 105 | Version: 1.25 106 | ------------- 107 | - adding waitForMasterPassword 108 | 109 | Version: 1.24 110 | ------------- 111 | - automatically localize i18n locale strings in injectElements() 112 | 113 | Version: 1.22 114 | ------------- 115 | - to reduce confusions, only check built-in URLs as add-on URLs cannot 116 | be resolved if a temp installed add-on has bin zipped 117 | 118 | Version: 1.21 119 | ------------- 120 | - print debug messages only if add-ons are installed temporarily from 121 | the add-on debug page 122 | - add checks to registered windows and scripts, if they actually exists 123 | 124 | Version: 1.20 125 | ------------- 126 | - fix long delay before customize window opens 127 | - fix non working removal of palette items 128 | 129 | Version: 1.19 130 | ------------- 131 | - add support for ToolbarPalette 132 | 133 | Version: 1.18 134 | ------------- 135 | - execute shutdown script also during global app shutdown (fixed) 136 | 137 | Version: 1.17 138 | ------------- 139 | - execute shutdown script also during global app shutdown 140 | 141 | Version: 1.16 142 | ------------- 143 | - support for persist 144 | 145 | Version: 1.15 146 | ------------- 147 | - make (undocumented) startup() async 148 | 149 | Version: 1.14 150 | ------------- 151 | - support resource urls 152 | 153 | Version: 1.12 154 | ------------- 155 | - no longer allow to enforce custom "namespace" 156 | - no longer call it namespace but uniqueRandomID / scopeName 157 | - expose special objects as the global WL object 158 | - autoremove injected elements after onUnload has ben executed 159 | 160 | Version: 1.9 161 | ------------- 162 | - automatically remove all entries added by injectElements 163 | 164 | Version: 1.8 165 | ------------- 166 | - add injectElements 167 | 168 | Version: 1.7 169 | ------------- 170 | - add injectCSS 171 | - add optional enforced namespace 172 | 173 | Version: 1.6 174 | ------------- 175 | - added mutation observer to be able to inject into browser elements 176 | - use larger icons as fallback 177 | -------------------------------------------------------------------------------- /content/api/NotifyTools/implementation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is provided by the addon-developer-support repository at 3 | * https://github.com/thundernest/addon-developer-support 4 | * 5 | * Version 1.5 6 | * - adjusted to Thunderbird Supernova (Services is now in globalThis) 7 | * 8 | * Version 1.4 9 | * - updated implementation to not assign this anymore 10 | * 11 | * Version 1.3 12 | * - moved registering the observer into startup 13 | * 14 | * Version 1.1 15 | * - added startup event, to make sure API is ready as soon as the add-on is starting 16 | * NOTE: This requires to add the startup event to the manifest, see: 17 | * https://github.com/thundernest/addon-developer-support/tree/master/auxiliary-apis/NotifyTools#usage 18 | * 19 | * Author: John Bieling (john@thunderbird.net) 20 | * 21 | * This Source Code Form is subject to the terms of the Mozilla Public 22 | * License, v. 2.0. If a copy of the MPL was not distributed with this 23 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 24 | */ 25 | 26 | "use strict"; 27 | 28 | (function (exports) { 29 | 30 | // Get various parts of the WebExtension framework that we need. 31 | var { ExtensionCommon } = ChromeUtils.importESModule( 32 | "resource://gre/modules/ExtensionCommon.sys.mjs" 33 | ); 34 | 35 | var observerTracker = new Set(); 36 | 37 | class NotifyTools extends ExtensionCommon.ExtensionAPI { 38 | getAPI(context) { 39 | return { 40 | NotifyTools: { 41 | 42 | notifyExperiment(data) { 43 | return new Promise(resolve => { 44 | Services.obs.notifyObservers( 45 | { data, resolve }, 46 | "NotifyExperimentObserver", 47 | context.extension.id 48 | ); 49 | }); 50 | }, 51 | 52 | onNotifyBackground: new ExtensionCommon.EventManager({ 53 | context, 54 | name: "NotifyTools.onNotifyBackground", 55 | register: (fire) => { 56 | observerTracker.add(fire.sync); 57 | return () => { 58 | observerTracker.delete(fire.sync); 59 | }; 60 | }, 61 | }).api(), 62 | 63 | } 64 | }; 65 | } 66 | 67 | // Force API to run at startup, otherwise event listeners might not be added at the requested time. Also needs 68 | // "events": ["startup"] in the experiment manifest 69 | onStartup() { 70 | this.onNotifyBackgroundObserver = async (aSubject, aTopic, aData) => { 71 | if ( 72 | observerTracker.size > 0 && 73 | aData == this.extension.id 74 | ) { 75 | let payload = aSubject.wrappedJSObject; 76 | 77 | // Make sure payload has a resolve function, which we use to resolve the 78 | // observer notification. 79 | if (payload.resolve) { 80 | let observerTrackerPromises = []; 81 | // Push listener into promise array, so they can run in parallel 82 | for (let listener of observerTracker.values()) { 83 | observerTrackerPromises.push(listener(payload.data)); 84 | } 85 | // We still have to await all of them but wait time is just the time needed 86 | // for the slowest one. 87 | let results = []; 88 | for (let observerTrackerPromise of observerTrackerPromises) { 89 | let rv = await observerTrackerPromise; 90 | if (rv != null) results.push(rv); 91 | } 92 | if (results.length == 0) { 93 | payload.resolve(); 94 | } else { 95 | if (results.length > 1) { 96 | console.warn( 97 | "Received multiple results from onNotifyBackground listeners. Using the first one, which can lead to inconsistent behavior.", 98 | results 99 | ); 100 | } 101 | payload.resolve(results[0]); 102 | } 103 | } else { 104 | // Older version of NotifyTools, which is not sending a resolve function, deprecated. 105 | console.log("Please update the notifyTools API and the notifyTools script to at least v1.5"); 106 | for (let listener of observerTracker.values()) { 107 | listener(payload.data); 108 | } 109 | } 110 | } 111 | }; 112 | 113 | // Add observer for notifyTools.js 114 | Services.obs.addObserver( 115 | this.onNotifyBackgroundObserver, 116 | "NotifyBackgroundObserver", 117 | false 118 | ); 119 | } 120 | 121 | onShutdown(isAppShutdown) { 122 | if (isAppShutdown) { 123 | return; // the application gets unloaded anyway 124 | } 125 | 126 | // Remove observer for notifyTools.js 127 | Services.obs.removeObserver( 128 | this.onNotifyBackgroundObserver, 129 | "NotifyBackgroundObserver" 130 | ); 131 | 132 | // Flush all caches 133 | Services.obs.notifyObservers(null, "startupcache-invalidate"); 134 | } 135 | }; 136 | 137 | exports.NotifyTools = NotifyTools; 138 | 139 | })(this) 140 | -------------------------------------------------------------------------------- /content/scripts/notifyTools.js: -------------------------------------------------------------------------------- 1 | // Set this to the ID of your add-on, or call notifyTools.setAddonID(). 2 | var ADDON_ID = "filtaquilla@mesquilla.com"; 3 | 4 | /* 5 | * This file is provided by the addon-developer-support repository at 6 | * https://github.com/thundernest/addon-developer-support 7 | * 8 | * For usage descriptions, please check: 9 | * https://github.com/thundernest/addon-developer-support/tree/master/scripts/notifyTools 10 | * 11 | * Version 1.6 12 | * - adjusted to Thunderbird Supernova (Services is now in globalThis) 13 | * 14 | * Version 1.5 15 | * - deprecate enable(), disable() and registerListener() 16 | * - add setAddOnId() 17 | * 18 | * Version 1.4 19 | * - auto enable/disable 20 | * 21 | * Version 1.3 22 | * - registered listeners for notifyExperiment can return a value 23 | * - remove WindowListener from name of observer 24 | * 25 | * Author: John Bieling (john@thunderbird.net) 26 | * 27 | * This Source Code Form is subject to the terms of the Mozilla Public 28 | * License, v. 2.0. If a copy of the MPL was not distributed with this 29 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 30 | */ 31 | 32 | var notifyTools = { 33 | registeredCallbacks: {}, 34 | registeredCallbacksNextId: 1, 35 | addOnId: ADDON_ID, 36 | 37 | setAddOnId: function (addOnId) { 38 | this.addOnId = addOnId; 39 | }, 40 | 41 | onNotifyExperimentObserver: { 42 | observe: async function (aSubject, aTopic, aData) { 43 | if (notifyTools.addOnId == "") { 44 | throw new Error("notifyTools: ADDON_ID is empty!"); 45 | } 46 | if (aData != notifyTools.addOnId) { 47 | return; 48 | } 49 | let payload = aSubject.wrappedJSObject; 50 | 51 | // Make sure payload has a resolve function, which we use to resolve the 52 | // observer notification. 53 | if (payload.resolve) { 54 | let observerTrackerPromises = []; 55 | // Push listener into promise array, so they can run in parallel 56 | for (let registeredCallback of Object.values( 57 | notifyTools.registeredCallbacks 58 | )) { 59 | observerTrackerPromises.push(registeredCallback(payload.data)); 60 | } 61 | // We still have to await all of them but wait time is just the time needed 62 | // for the slowest one. 63 | let results = []; 64 | for (let observerTrackerPromise of observerTrackerPromises) { 65 | let rv = await observerTrackerPromise; 66 | if (rv != null) results.push(rv); 67 | } 68 | if (results.length == 0) { 69 | payload.resolve(); 70 | } else { 71 | if (results.length > 1) { 72 | console.warn( 73 | "Received multiple results from onNotifyExperiment listeners. Using the first one, which can lead to inconsistent behavior.", 74 | results 75 | ); 76 | } 77 | payload.resolve(results[0]); 78 | } 79 | } else { 80 | // Older version of NotifyTools, which is not sending a resolve function, deprecated. 81 | console.log("Please update the notifyTools API to at least v1.5"); 82 | for (let registeredCallback of Object.values( 83 | notifyTools.registeredCallbacks 84 | )) { 85 | registeredCallback(payload.data); 86 | } 87 | } 88 | }, 89 | }, 90 | 91 | addListener: function (listener) { 92 | if (Object.values(this.registeredCallbacks).length == 0) { 93 | Services.obs.addObserver( 94 | this.onNotifyExperimentObserver, 95 | "NotifyExperimentObserver", 96 | false 97 | ); 98 | } 99 | 100 | let id = this.registeredCallbacksNextId++; 101 | this.registeredCallbacks[id] = listener; 102 | return id; 103 | }, 104 | 105 | removeListener: function (id) { 106 | delete this.registeredCallbacks[id]; 107 | if (Object.values(this.registeredCallbacks).length == 0) { 108 | Services.obs.removeObserver( 109 | this.onNotifyExperimentObserver, 110 | "NotifyExperimentObserver" 111 | ); 112 | } 113 | }, 114 | 115 | removeAllListeners: function () { 116 | if (Object.values(this.registeredCallbacks).length != 0) { 117 | Services.obs.removeObserver( 118 | this.onNotifyExperimentObserver, 119 | "NotifyExperimentObserver" 120 | ); 121 | } 122 | this.registeredCallbacks = {}; 123 | }, 124 | 125 | notifyBackground: function (data) { 126 | if (this.addOnId == "") { 127 | throw new Error("notifyTools: ADDON_ID is empty!"); 128 | } 129 | return new Promise((resolve) => { 130 | Services.obs.notifyObservers( 131 | { data, resolve }, 132 | "NotifyBackgroundObserver", 133 | this.addOnId 134 | ); 135 | }); 136 | }, 137 | 138 | 139 | // Deprecated. 140 | 141 | enable: function () { 142 | console.log("Manually calling notifyTools.enable() is no longer needed."); 143 | }, 144 | 145 | disable: function () { 146 | console.log("notifyTools.disable() has been deprecated, use notifyTools.removeAllListeners() instead."); 147 | this.removeAllListeners(); 148 | }, 149 | 150 | registerListener: function (listener) { 151 | console.log("notifyTools.registerListener() has been deprecated, use notifyTools.addListener() instead."); 152 | this.addListener(listener); 153 | }, 154 | 155 | }; 156 | 157 | if (typeof window != "undefined" && window) { 158 | window.addEventListener( 159 | "unload", 160 | function (event) { 161 | notifyTools.removeAllListeners(); 162 | }, 163 | false 164 | ); 165 | } 166 | -------------------------------------------------------------------------------- /html/popup.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 | // eslint-disable-next-line no-unused-vars 26 | function hide(id) { 27 | let el = document.getElementById(id); 28 | if (!el) { 29 | return null; 30 | } 31 | el.setAttribute("collapsed", true); 32 | return el; 33 | } 34 | 35 | // eslint-disable-next-line no-unused-vars 36 | function hideSelectorItems(cId) { 37 | let elements = document.querySelectorAll(cId); 38 | for (let el of elements) { 39 | el.setAttribute("collapsed", true); 40 | } 41 | } 42 | 43 | // eslint-disable-next-line no-unused-vars 44 | function show(id) { 45 | let el = document.getElementById(id); 46 | if (!el) { 47 | return null; 48 | } 49 | el.setAttribute("collapsed", false); 50 | return el; 51 | } 52 | 53 | const formatAll = (txt) => { 54 | let localizedMsg = txt; 55 | return localizedMsg 56 | .replace(/\{bold\}/g, "") 57 | .replace(/\{\/bold\}/g, "") 58 | .replace(/\{hr\}/g, "
") 59 | .replace(/\{italic\}/g, "") 60 | .replace(/\{\/italic\}/g, "") 61 | .replace(/\{U\}/g, "
    ") 62 | .replace(/\{\/U\}/g, "
") 63 | .replace(/\{L\}/g, "
  • ") 64 | .replace(/\{\/L\}/g, "
  • ") 65 | .replace(/\{P(?:\s+([^}]+))?\}/g, (_, attrs) => { 66 | return attrs ? `

    ` : "

    "; 67 | }) 68 | .replace(/\{\/P\}/g, "

    ") 69 | .replace( 70 | /\{ARelease\}/g, 71 | "" 72 | ) 73 | .replace( 74 | /\{AcompatCheck\}/g, 75 | "" 76 | ) 77 | .replace( 78 | /\{A-QF\}/g, 79 | "" 80 | ) 81 | .replace( 82 | /\{A-qI\}/g, 83 | "" 84 | ) 85 | .replace( 86 | /\{A-ST\}/g, 87 | "" 88 | ) 89 | .replace(/\{\/A\}/g, "") 90 | .replace(/\{A\}/g, "") 91 | .replace(/\{br\}/g, "
    ") 92 | .replace(/\[issue (\d+)\]/g, "[issue $1]") 93 | .replace(/\[Bug (\d+)\]/g, "[Bug $1]") 94 | .replace(/\[(.)\]/g, "$1") 95 | .replace(/\[(F\d+)\]/g, "$1") 96 | .replace(/\[(CTRL|ALT|SHIFT)\]/g, "$1"); 97 | }; 98 | 99 | 100 | // eslint-disable-next-line no-unused-vars 101 | async function insertLocalizedMessage(element, rawMessage) { 102 | try { 103 | const html = formatAll(rawMessage); // Expand custom tags into HTML 104 | const fragment = parseHTMLFragment(html); // Safely parse into a DocumentFragment 105 | element.textContent = ""; // Clear existing content 106 | 107 | element.appendChild(fragment); // Inject parsed content 108 | } catch (ex) { 109 | console.error("Failed to parse localized message:", ex); 110 | element.textContent = rawMessage; // Fallback: insert raw text only 111 | } 112 | } 113 | 114 | // replace unsafe innerHTML injections 115 | // note: this will add closing tags and other markup ,e.g. or 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 | ![messaging](https://user-images.githubusercontent.com/5830621/111921572-90db8d80-8a95-11eb-8673-4e1370d49e4b.png) 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": "グローバル