├── .gitignore ├── .prettierignore ├── images ├── icon.png ├── icon-16px.png ├── icon-32px.png ├── screenshot.jpg ├── score_neutral.png ├── score_negative.png ├── score_positive.png ├── screenshot_settings.jpg ├── score_no.svg ├── score_neutral.svg ├── score_positive.svg └── score_negative.svg ├── src ├── background │ ├── background.html │ └── background.js ├── static │ ├── hello.css │ ├── hello.js │ └── hello.html ├── browser │ └── popup │ │ ├── popup.html │ │ ├── popup.css │ │ └── popup.js ├── typedef │ ├── globals.js │ └── general.js ├── functions.js ├── options │ ├── options.css │ ├── options.html │ └── options.js ├── experiments │ ├── schema.json │ └── experiments.js └── constants.js ├── .prettierrc ├── test └── mail-examples │ ├── vr-spamscore-1.eml │ ├── rspamd-report-1.eml │ ├── spam-status-5.eml │ ├── spamd-result-1.eml │ ├── spam-status-3.eml │ ├── spam-status-2.eml │ ├── spam-status-7.eml │ ├── spam-status-1.eml │ ├── mailscanner-spamscore-1.eml │ ├── spam-status-4.eml │ ├── spam-status-6.eml │ ├── spam-status-8.eml │ └── spam-report-1.eml ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── manifest.json ├── _locales ├── en │ └── messages.json ├── es │ └── messages.json ├── de │ └── messages.json └── it │ └── messages.json ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | *.xpi -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | rspamd_symbols.js -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/friedPotat0/Spam-Scores/HEAD/images/icon.png -------------------------------------------------------------------------------- /images/icon-16px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/friedPotat0/Spam-Scores/HEAD/images/icon-16px.png -------------------------------------------------------------------------------- /images/icon-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/friedPotat0/Spam-Scores/HEAD/images/icon-32px.png -------------------------------------------------------------------------------- /images/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/friedPotat0/Spam-Scores/HEAD/images/screenshot.jpg -------------------------------------------------------------------------------- /images/score_neutral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/friedPotat0/Spam-Scores/HEAD/images/score_neutral.png -------------------------------------------------------------------------------- /images/score_negative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/friedPotat0/Spam-Scores/HEAD/images/score_negative.png -------------------------------------------------------------------------------- /images/score_positive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/friedPotat0/Spam-Scores/HEAD/images/score_positive.png -------------------------------------------------------------------------------- /images/screenshot_settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/friedPotat0/Spam-Scores/HEAD/images/screenshot_settings.jpg -------------------------------------------------------------------------------- /src/background/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/static/hello.css: -------------------------------------------------------------------------------- 1 | /* COLOR SETTINGS FOR DARK/LIGHT THEME */ 2 | @media (prefers-color-scheme: light) { 3 | body { 4 | color: black; 5 | background: white; 6 | } 7 | } 8 | 9 | @media (prefers-color-scheme: dark) { 10 | body { 11 | color: white; 12 | background: #2b2a33; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 120, 7 | "useTabs": false, 8 | "quoteProps": "as-needed", 9 | "bracketSpacing": true, 10 | "jsxBracketSameLine": true, 11 | "arrowParens": "avoid", 12 | "vueIndentScriptAndStyle": false, 13 | "endOfLine": "lf", 14 | "htmlWhitespaceSensitivity": "css" 15 | } 16 | -------------------------------------------------------------------------------- /src/static/hello.js: -------------------------------------------------------------------------------- 1 | const i18n = messenger.i18n 2 | for (const i18nKey of [ 3 | 'helloHeadline', 4 | 'helloParagraph1', 5 | 'helloParagraph2', 6 | 'helloParagraph3', 7 | 'helloParagraph4', 8 | 'helloInstruction_1', 9 | 'helloInstruction_2', 10 | 'helloInstruction_3', 11 | 'helloFooterParagraph1', 12 | 'helloFooterParagraph2' 13 | ]) { 14 | document.querySelector('*[data-i18n="' + i18nKey + '"]').innerHTML = i18n.getMessage(i18nKey) 15 | } 16 | document.title = i18n.getMessage('helloPopupTitle') 17 | -------------------------------------------------------------------------------- /test/mail-examples/vr-spamscore-1.eml: -------------------------------------------------------------------------------- 1 | To: receiver@spamscores 2 | From: sender@spamscores 3 | Subject: [X-VR-SPAMSCORE] Spam Scores test mail (vr-spamscore-1.eml) 4 | X-VR-SPAMSTATE: SPAM 5 | X-VR-SPAMSCORE: 420 6 | X-VR-SPAMCAUSE: [...] 7 | 8 | This is a message to test the Spam Scores Thunderbird add-on. 9 | 10 | -- 11 | To: receiver@spamscores 12 | From: sender@spamscores 13 | Subject: [X-VR-SPAMSCORE] Spam Scores test mail (vr-spamscore-1.eml) 14 | X-VR-SPAMSTATE: SPAM 15 | X-VR-SPAMSCORE: 420 16 | X-VR-SPAMCAUSE: [...] -------------------------------------------------------------------------------- /images/score_no.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /images/score_neutral.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /images/score_positive.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /images/score_negative.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | score_negative 8 | 11 | 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /test/mail-examples/rspamd-report-1.eml: -------------------------------------------------------------------------------- 1 | To: receiver@spamscores 2 | From: sender@spamscores 3 | Subject: [X-Rspamd-Report] Spam Scores test mail (rspamd-report-1.eml) 4 | X-Rspamd-Bar: / 5 | X-Rspamd-Report: XM_UA_NO_VERSION(0.01) MIME_GOOD(-0.1) MIME_BASE64_TEXT(0.1) MISSING_TO(2) RCVD_NO_TLS_LAST(0.1) BAYES_HAM(-2.999991) 6 | X-Rspamd-Score: -42.42 7 | 8 | This is a message to test the Spam Scores Thunderbird add-on. 9 | 10 | -- 11 | To: receiver@spamscores 12 | From: sender@spamscores 13 | Subject: [X-Rspamd-Report] Spam Scores test mail (rspamd-report-1.eml) 14 | X-Rspamd-Bar: / 15 | X-Rspamd-Report: XM_UA_NO_VERSION(0.01) MIME_GOOD(-0.1) MIME_BASE64_TEXT(0.1) MISSING_TO(2) RCVD_NO_TLS_LAST(0.1) BAYES_HAM(-2.999991) 16 | X-Rspamd-Score: -42.42 -------------------------------------------------------------------------------- /test/mail-examples/spam-status-5.eml: -------------------------------------------------------------------------------- 1 | To: receiver@spamscores 2 | From: sender@spamscores 3 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-5.eml) 4 | X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on srv01.spamscores 5 | X-Spam-Level: 6 | X-Spam-Status: No, score=-42.42 required=5.0 tests=R_DKIM_ALLOW, 7 | R_MIXED_CHARSET,FREEMAIL_FROM,DKIM_TRACE,MX_GOOD,BAYES_HAM 8 | Message-ID: spam-status-5 9 | 10 | This is a message to test the Spam Scores Thunderbird add-on. 11 | 12 | -- 13 | To: receiver@spamscores 14 | From: sender@spamscores 15 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-5.eml) 16 | X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on srv01.spamscores 17 | X-Spam-Level: 18 | X-Spam-Status: No, score=-42.42 required=5.0 tests=R_DKIM_ALLOW, 19 | R_MIXED_CHARSET,FREEMAIL_FROM,DKIM_TRACE,MX_GOOD,BAYES_HAM 20 | Message-ID: spam-status-5 -------------------------------------------------------------------------------- /test/mail-examples/spamd-result-1.eml: -------------------------------------------------------------------------------- 1 | To: receiver@spamscores 2 | From: sender@spamscores 3 | Subject: [X-Spamd-Result] Spam Scores test mail (spamd-result-1.eml) 4 | X-Spamd-Result: default: False [-42.42 / 15.00]; 5 | R_DKIM_ALLOW(-0.2)[]; 6 | R_MIXED_CHARSET(0.18)[]; 7 | FREEMAIL_FROM(0.00)[gmx.de]; 8 | DKIM_TRACE(-0.00)[]; 9 | MX_GOOD(-0.01)[]; 10 | BAYES_HAM(-5.5)[]; 11 | Message-ID: spamd-result-1 12 | X-Spam-Flag: NO 13 | 14 | This is a message to test the Spam Scores Thunderbird add-on. 15 | 16 | -- 17 | To: receiver@spamscores 18 | From: sender@spamscores 19 | Subject: [X-Spamd-Result] Spam Scores test mail (spamd-result-1.eml) 20 | X-Spamd-Result: default: False [-42.42 / 15.00]; 21 | R_DKIM_ALLOW(-0.2)[]; 22 | R_MIXED_CHARSET(0.18)[]; 23 | FREEMAIL_FROM(0.00)[gmx.de]; 24 | DKIM_TRACE(-0.00)[]; 25 | MX_GOOD(-0.01)[]; 26 | BAYES_HAM(-5.5)[]; 27 | Message-ID: spamd-result-1 28 | X-Spam-Flag: NO -------------------------------------------------------------------------------- /src/browser/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | popup.html 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
ScoreNameDescription
18 |
No details available
19 | 20 | 21 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/static/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |

12 |

13 |
    14 |
  1. 15 |
  2. 16 |
  3. 17 |
18 |

19 |

20 |

21 |
22 |

23 |

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /test/mail-examples/spam-status-3.eml: -------------------------------------------------------------------------------- 1 | To: receiver@spamscores 2 | From: sender@spamscores 3 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-3.eml) 4 | X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on srv01.spamscores 5 | X-Spam-Level: 6 | X-Spam-Status: No, score=-42.42 required=3.0 tests=R_DKIM_ALLOW,R_MIXED_CHARSET, 7 | FREEMAIL_FROM,DKIM_TRACE,MX_GOOD,BAYES_HAM 8 | autolearn=ham autolearn_force=no version=3.4.0 9 | Message-ID: spam-status-3 10 | 11 | This is a message to test the Spam Scores Thunderbird add-on. 12 | 13 | -- 14 | To: receiver@spamscores 15 | From: sender@spamscores 16 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-3.eml) 17 | X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on srv01.spamscores 18 | X-Spam-Level: 19 | X-Spam-Status: No, score=-42.42 required=3.0 tests=R_DKIM_ALLOW,R_MIXED_CHARSET, 20 | FREEMAIL_FROM,DKIM_TRACE,MX_GOOD,BAYES_HAM 21 | autolearn=ham autolearn_force=no version=3.4.0 22 | Message-ID: spam-status-3 -------------------------------------------------------------------------------- /src/typedef/globals.js: -------------------------------------------------------------------------------- 1 | // FIREFOX GLOBALS 2 | 3 | // https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/basics.html#globals-available-in-the-api-scripts-global 4 | 5 | /** 6 | * Services namespace 7 | * Based on Mozilla xpCOM 8 | * https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/57#changes_for_add-on_and_mozilla_developers 9 | * @namespace 10 | * @property {object} xulStore 11 | * @property {object} obs https://web.archive.org/web/20210603143450/https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIObserverService 12 | * @property {object} scriptloader 13 | */ 14 | const Services = {} 15 | 16 | /** 17 | * AppConstants 18 | * @namespace 19 | * @property {number} MOZ_APP_VERSION Thunderbird Version 20 | */ 21 | const AppConstants = {} 22 | 23 | /** 24 | * WebExtensionPolicy 25 | * @namespace 26 | */ 27 | const WebExtensionPolicy = { 28 | /** 29 | * 30 | * @param {*} id 31 | * @returns {extension} 32 | */ 33 | getByID: id => {} 34 | } 35 | -------------------------------------------------------------------------------- /test/mail-examples/spam-status-2.eml: -------------------------------------------------------------------------------- 1 | To: receiver@spamscores 2 | From: sender@spamscores 3 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-2.eml) 4 | X-Spam-Flag: NO 5 | X-Spam-Score: -42.42 6 | X-Spam-Level: 7 | X-Spam-Status: No, score=-42.42 tagged_above=-9999 required=3 8 | tests=[R_DKIM_ALLOW=-0.2, R_MIXED_CHARSET=0.18, 9 | FREEMAIL_FROM=0.00, DKIM_TRACE=-0.00, MX_GOOD=-0.01, 10 | BAYES_HAM=-5.5] autolearn=ham autolearn_force=no 11 | Message-ID: spam-status-2 12 | X-Spam-Flag: NO 13 | 14 | This is a message to test the Spam Scores Thunderbird add-on. 15 | 16 | -- 17 | To: receiver@spamscores 18 | From: sender@spamscores 19 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-2.eml) 20 | X-Spam-Flag: NO 21 | X-Spam-Score: -42.42 22 | X-Spam-Level: 23 | X-Spam-Status: No, score=-42.42 tagged_above=-9999 required=3 24 | tests=[R_DKIM_ALLOW=-0.2, R_MIXED_CHARSET=0.18, 25 | FREEMAIL_FROM=0.00, DKIM_TRACE=-0.00, MX_GOOD=-0.01, 26 | BAYES_HAM=-5.5] autolearn=ham autolearn_force=no 27 | Message-ID: spam-status-2 28 | X-Spam-Flag: NO -------------------------------------------------------------------------------- /test/mail-examples/spam-status-7.eml: -------------------------------------------------------------------------------- 1 | To: receiver@spamscores 2 | From: sender@spamscores 3 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-7.eml) 4 | X-Spam-Report: -42.42 hits, 9 required; 5 | * -0.2 -- DKIM verification succeed 6 | * 0.18 -- Mixed characters in a message 7 | * 0.00 -- From is a Freemail address 8 | [gmx.de] 9 | * -0.00 -- DKIM checks completed 10 | * -0.01 -- Domain has working MX 11 | * -5.5 -- Message probably ham 12 | Message-ID: spam-status-7 13 | 14 | This is a message to test the Spam Scores Thunderbird add-on. 15 | 16 | -- 17 | To: receiver@spamscores 18 | From: sender@spamscores 19 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-7.eml) 20 | X-Spam-Report: -42.42 hits, 9 required; 21 | * -0.2 -- DKIM verification succeed 22 | * 0.18 -- Mixed characters in a message 23 | * 0.00 -- From is a Freemail address 24 | [gmx.de] 25 | * -0.00 -- DKIM checks completed 26 | * -0.01 -- Domain has working MX 27 | * -5.5 -- Message probably ham 28 | Message-ID: spam-status-7 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help improve the project 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Reproduction steps** 14 | Steps to reproduce the behavior. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Thunderbird (please complete the following information):** 23 | - OS: [e.g. Windows 10] 24 | - Version: [e.g. 78.7.0] 25 | 26 | **Spam Score add-on (please complete the following information):** 27 | - Version: [e.g. 1.1.5] - visible under Extras -> Add-ons -> Spam Scores 28 | 29 | **Additional context** 30 | Add any other context about the problem here. If, for example, no spam score is visible in the list view column, please provide an example of a header of any of these mails (blank out private information like IP or mail addresses). 31 | -------------------------------------------------------------------------------- /test/mail-examples/spam-status-1.eml: -------------------------------------------------------------------------------- 1 | To: receiver@spamscores 2 | From: sender@spamscores 3 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-1.eml) 4 | X-Spam-Status: No, score=-42.42 5 | X-Spam-Score: -42.42 6 | X-Spam-Bar: + 7 | X-Spam-Report: Action: no action 8 | Symbol: R_DKIM_ALLOW(-0.2) 9 | Symbol: R_MIXED_CHARSET(0.18) 10 | Symbol: FREEMAIL_FROM(0.00) 11 | Symbol: DKIM_TRACE(-0.00) 12 | Symbol: MX_GOOD(-0.01) 13 | Symbol: BAYES_HAM(-5.5) 14 | Message-ID: spam-status-1 15 | X-Spam-Flag: NO 16 | 17 | This is a message to test the Spam Scores Thunderbird add-on. 18 | 19 | -- 20 | To: receiver@spamscores 21 | From: sender@spamscores 22 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-1.eml) 23 | X-Spam-Status: No, score=-42.42 24 | X-Spam-Score: -42.42 25 | X-Spam-Bar: + 26 | X-Spam-Report: Action: no action 27 | Symbol: R_DKIM_ALLOW(-0.2) 28 | Symbol: R_MIXED_CHARSET(0.18) 29 | Symbol: FREEMAIL_FROM(0.00) 30 | Symbol: DKIM_TRACE(-0.00) 31 | Symbol: MX_GOOD(-0.01) 32 | Symbol: BAYES_HAM(-5.5) 33 | Message-ID: spam-status-1 34 | X-Spam-Flag: NO -------------------------------------------------------------------------------- /test/mail-examples/mailscanner-spamscore-1.eml: -------------------------------------------------------------------------------- 1 | To: receiver@spamscores 2 | From: sender@spamscores 3 | Subject: [X-MyCompany-MailScanner-SpamScore] Spam Scores test mail (mailscanner-spamscore-1.eml) 4 | X-MyCompany-MailScanner-From: receiver@spamscores 5 | X-MyCompany-MailScanner-SpamScore: ssssssss 6 | X-MyCompany-MailScanner-SpamCheck: spam, SpamAssassin (not cached, score=-42.42, 7 | required 3, R_DKIM_ALLOW -0.2, R_MIXED_CHARSET 0.18, 8 | FREEMAIL_FROM 0.00, DKIM_TRACE -0.00, MX_GOOD -0.01, 9 | BAYES_HAM -5.5) 10 | Message-ID: mailscanner-spamscore-1 11 | 12 | This is a message to test the Spam Scores Thunderbird add-on. 13 | 14 | -- 15 | To: receiver@spamscores 16 | From: sender@spamscores 17 | Subject: [X-MyCompany-MailScanner-SpamScore] Spam Scores test mail (mailscanner-spamscore-1.eml) 18 | X-MyCompany-MailScanner-From: receiver@spamscores 19 | X-MyCompany-MailScanner-SpamScore: ssssssss 20 | X-MyCompany-MailScanner-SpamCheck: spam, SpamAssassin (not cached, score=-42.42, 21 | required 3, R_DKIM_ALLOW -0.2, R_MIXED_CHARSET 0.18, 22 | FREEMAIL_FROM 0.00, DKIM_TRACE -0.00, MX_GOOD -0.01, 23 | BAYES_HAM -5.5) 24 | Message-ID: mailscanner-spamscore-1 -------------------------------------------------------------------------------- /test/mail-examples/spam-status-4.eml: -------------------------------------------------------------------------------- 1 | To: receiver@spamscores 2 | From: sender@spamscores 3 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-4.eml) 4 | X-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on srv01.spamscores 5 | X-Spam-Level: 6 | X-Spam-Status: No/-42.42/5.0 7 | X-Spam-Report: Action: no action 8 | Symbol: R_DKIM_ALLOW(-0.2) 9 | Symbol: R_MIXED_CHARSET(0.18) 10 | Symbol: FREEMAIL_FROM(0.00) 11 | Symbol: DKIM_TRACE(-0.00) 12 | Symbol: MX_GOOD(-0.01) 13 | Symbol: BAYES_HAM(-5.5) 14 | Message-ID: spam-status-4 15 | X-Spam-Flag: NO 16 | 17 | This is a message to test the Spam Scores Thunderbird add-on. 18 | 19 | -- 20 | To: receiver@spamscores 21 | From: sender@spamscores 22 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-4.eml) 23 | X-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on srv01.spamscores 24 | X-Spam-Level: 25 | X-Spam-Status: No/-42.42/5.0 26 | X-Spam-Report: Action: no action 27 | Symbol: R_DKIM_ALLOW(-0.2) 28 | Symbol: R_MIXED_CHARSET(0.18) 29 | Symbol: FREEMAIL_FROM(0.00) 30 | Symbol: DKIM_TRACE(-0.00) 31 | Symbol: MX_GOOD(-0.01) 32 | Symbol: BAYES_HAM(-5.5) 33 | Message-ID: spam-status-4 34 | X-Spam-Flag: NO -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "__MSG_extensionName__", 4 | "description": "__MSG_extensionDescription__", 5 | "version": "1.6.0", 6 | "author": "Christian Zaenker", 7 | "default_locale": "en", 8 | "applications": { 9 | "gecko": { 10 | "id": "spamscores@czaenker", 11 | "strict_min_version": "115.0", 12 | "strict_max_version": "144.*" 13 | } 14 | }, 15 | "icons": { 16 | "64": "images/icon.png", 17 | "32": "images/icon-32px.png", 18 | "16": "images/icon-16px.png" 19 | }, 20 | "background": { 21 | "page": "src/background/background.html" 22 | }, 23 | "permissions": ["messagesRead", "accountsRead", "storage"], 24 | "experiment_apis": { 25 | "SpamScores": { 26 | "schema": "src/experiments/schema.json", 27 | "parent": { 28 | "scopes": ["addon_parent"], 29 | "paths": [["SpamScores"]], 30 | "script": "src/experiments/experiments.js", 31 | "events": ["startup"] 32 | } 33 | } 34 | }, 35 | "message_display_action": { 36 | "default_popup": "src/browser/popup/popup.html", 37 | "default_title": "Spam Scores" 38 | }, 39 | "options_ui": { 40 | "page": "src/options/options.html", 41 | "open_in_tab": false, 42 | "browser_style": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/mail-examples/spam-status-6.eml: -------------------------------------------------------------------------------- 1 | To: receiver@spamscores 2 | From: sender@spamscores 3 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-6.eml) 4 | X-Spam-Status: Yes, score=-42.42 required=5.0 tests=R_DKIM_ALLOW,R_MIXED_CHARSET, 5 | FREEMAIL_FROM,DKIM_TRACE,MX_GOOD,BAYES_HAM autolearn=no version=3.3.2 6 | X-Spam-Report: 7 | * -0.2 R_DKIM_ALLOW DKIM verification succeed 8 | * 0.18 R_MIXED_CHARSET Mixed characters in a message 9 | * 0.00 FREEMAIL_FROM From is a Freemail address 10 | * [gmx.de] 11 | * -0.00 DKIM_TRACE DKIM checks completed 12 | * -0.01 MX_GOOD Domain has working MX 13 | * -5.5 BAYES_HAM Message probably ham 14 | X-Spam-Level: 15 | X-Spam-Checker-Version: SpamAssassin 3.3.2 (2011-06-06) on srv01.spamscores 16 | Message-ID: spam-status-6 17 | 18 | This is a message to test the Spam Scores Thunderbird add-on. 19 | 20 | -- 21 | To: receiver@spamscores 22 | From: sender@spamscores 23 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-6.eml) 24 | X-Spam-Status: Yes, score=-42.42 required=5.0 tests=R_DKIM_ALLOW,R_MIXED_CHARSET, 25 | FREEMAIL_FROM,DKIM_TRACE,MX_GOOD,BAYES_HAM autolearn=no version=3.3.2 26 | X-Spam-Report: 27 | * -0.2 R_DKIM_ALLOW DKIM verification succeed 28 | * 0.18 R_MIXED_CHARSET Mixed characters in a message 29 | * 0.00 FREEMAIL_FROM From is a Freemail address 30 | * [gmx.de] 31 | * -0.00 DKIM_TRACE DKIM checks completed 32 | * -0.01 MX_GOOD Domain has working MX 33 | * -5.5 BAYES_HAM Message probably ham 34 | X-Spam-Level: 35 | X-Spam-Checker-Version: SpamAssassin 3.3.2 (2011-06-06) on srv01.spamscores 36 | Message-ID: spam-status-6 -------------------------------------------------------------------------------- /src/functions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Functions module. 3 | * @module functions 4 | */ 5 | import { 6 | DEFAULT_SCORE_LOWER_BOUNDS, 7 | DEFAULT_SCORE_UPPER_BOUNDS 8 | // MAX_SCORE_SEEN, 9 | // MIN_SCORE_SEEN, 10 | // SCORE_INTERPOLATION 11 | } from './constants.js' 12 | 13 | /** 14 | * Returns lower & upper bounds 15 | * @param {Object} storage Local storage with lower & upper bounds 16 | * @returns {number[]} Lower & upper bounds 17 | */ 18 | export function getBounds(storage) { 19 | const lowerBounds = parseFloat(storage.scoreIconLowerBounds || DEFAULT_SCORE_LOWER_BOUNDS) 20 | const upperBounds = parseFloat(storage.scoreIconUpperBounds || DEFAULT_SCORE_UPPER_BOUNDS) 21 | return [lowerBounds, upperBounds] 22 | } 23 | 24 | // /** 25 | // * 26 | // * @param {string} headername 27 | // * @param {string} score 28 | // * @returns {string} Interpolated score or the raw score 29 | // */ 30 | // export function scoreInterpolation(headername, score) { 31 | // const scoreType = SCORE_INTERPOLATION[headername] 32 | // if (scoreType) { 33 | // let numerator = 1 34 | // let denominator = 1 35 | // if (score > scoreType.MAX_UPPER_BOUNDS) { 36 | // numerator = MAX_SCORE_SEEN - DEFAULT_SCORE_UPPER_BOUNDS 37 | // denominator = scoreType.MAX_VALUE - scoreType.UPPER_BOUNDS 38 | // } else if (score < scoreType.MIN_LOWER_BOUNDS) { 39 | // numerator = MIN_SCORE_SEEN - DEFAULT_SCORE_LOWER_BOUNDS 40 | // denominator = scoreType.MIN_VALUE - scoreType.LOWER_BOUNDS 41 | // } else { 42 | // numerator = DEFAULT_SCORE_UPPER_BOUNDS - DEFAULT_SCORE_LOWER_BOUNDS 43 | // denominator = scoreType.UPPER_BOUNDS - scoreType.LOWER_BOUNDS 44 | // } 45 | // const scale = numerator / denominator 46 | // score *= scale 47 | // } 48 | // return score 49 | // } 50 | -------------------------------------------------------------------------------- /src/browser/popup/popup.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0px; 3 | } 4 | h4 { 5 | margin: 0 4px 10px 4px; 6 | } 7 | h5#no-details { 8 | margin: 0; 9 | display: none; 10 | } 11 | table#score-details { 12 | border-collapse: collapse; 13 | margin: 0; 14 | padding: 0; 15 | padding-bottom: 5px; 16 | width: 100%; 17 | font-size: 14px; 18 | display: none; 19 | } 20 | table#score-details tr th { 21 | text-align: left; 22 | padding: 5px; 23 | } 24 | table#score-details tr td { 25 | vertical-align: top; 26 | padding: 5px; 27 | } 28 | table#score-details tr:last-child td { 29 | border-bottom: none; 30 | } 31 | table#score-details tr th:nth-child(1) { 32 | padding: 0 20px; 33 | } 34 | table#score-details tr td:nth-child(1) { 35 | text-align: center; 36 | padding: 5px 20px; 37 | } 38 | table#score-details tr td:nth-child(2), 39 | table#score-details tr td:nth-child(3) { 40 | padding: 7px 5px; 41 | } 42 | table#score-details tr td:nth-child(3) { 43 | max-width: 400px; 44 | } 45 | table#score-details tr.score-detail-row td div.info { 46 | word-break: break-all; 47 | } 48 | table#score-details tr.score-detail-row .score { 49 | display: inline-block; 50 | padding: 2px 4px; 51 | border-radius: 3px; 52 | color: white; 53 | width: 100%; 54 | } 55 | table#score-details tr.score-detail-row.negative .score { 56 | background-color: #28b62c; 57 | } 58 | table#score-details tr.score-detail-row.positive .score { 59 | background-color: #fe330a; 60 | } 61 | table#score-details tr.score-detail-row.neutral .score { 62 | background-color: #a1a1a1; 63 | } 64 | 65 | /* COLOR SETTINGS FOR DARK/LIGHT THEME */ 66 | @media (prefers-color-scheme: light) { 67 | body { 68 | color: black; 69 | background: white; 70 | } 71 | table#score-details tr.score-detail-row td div.info { 72 | color: #7a7a7a; 73 | } 74 | table#score-details tr td { 75 | border-bottom: 1px solid #eee; 76 | } 77 | } 78 | 79 | @media (prefers-color-scheme: dark) { 80 | body { 81 | color: white; 82 | background: #2b2a33; 83 | } 84 | table#score-details tr.score-detail-row td div.info { 85 | color: #a0a0a0; 86 | } 87 | table#score-details tr td { 88 | border-bottom: 1px solid #464646; 89 | } 90 | table#score-details tr.score-detail-row.neutral .score { 91 | background-color: #6d6d6d; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/options/options.css: -------------------------------------------------------------------------------- 1 | .input-line * { 2 | display: inline-block; 3 | vertical-align: middle; 4 | margin-top: 10px; 5 | font-size: 14px; 6 | } 7 | .input-line img { 8 | margin-right: 5px; 9 | } 10 | .checkbox-wrapper { 11 | line-height: 40px; 12 | font-size: 14px; 13 | margin-left: 30px; 14 | } 15 | .checkbox-wrapper * { 16 | display: inline-block; 17 | vertical-align: middle; 18 | } 19 | 20 | .sortable-list { 21 | border: 1px solid #ccc; 22 | border-radius: 4px; 23 | padding: 10px; 24 | max-width: 400px; 25 | min-height: 50px; 26 | } 27 | 28 | .sortable-item { 29 | background: #f5f5f5; 30 | border: 1px solid #ddd; 31 | padding: 8px 12px; 32 | margin: 5px 0; 33 | border-radius: 3px; 34 | cursor: move; 35 | user-select: none; 36 | display: flex; 37 | align-items: center; 38 | font-family: monospace; 39 | font-size: 13px; 40 | } 41 | 42 | .sortable-item:hover { 43 | background: #e8e8e8; 44 | } 45 | 46 | .sortable-item.dragging { 47 | opacity: 0.5; 48 | } 49 | 50 | .sortable-item::before { 51 | content: '⋮⋮'; 52 | margin-right: 10px; 53 | color: #999; 54 | font-size: 16px; 55 | font-weight: bold; 56 | } 57 | 58 | .reset-button { 59 | padding: 6px 12px; 60 | background: #f0f0f0; 61 | border: 1px solid #ccc; 62 | border-radius: 3px; 63 | cursor: pointer; 64 | font-size: 13px; 65 | } 66 | 67 | .reset-button:hover { 68 | background: #e0e0e0; 69 | } 70 | 71 | .reset-button:active { 72 | background: #d0d0d0; 73 | } 74 | 75 | /* COLOR SETTINGS FOR DARK/LIGHT THEME */ 76 | @media (prefers-color-scheme: light) { 77 | body { 78 | color: black; 79 | background: white; 80 | } 81 | } 82 | 83 | @media (prefers-color-scheme: dark) { 84 | body { 85 | color: white; 86 | background: #23222b; 87 | } 88 | 89 | .sortable-list { 90 | border-color: #555; 91 | } 92 | 93 | .sortable-item { 94 | background: #333; 95 | border-color: #555; 96 | color: white; 97 | } 98 | 99 | .sortable-item:hover { 100 | background: #444; 101 | } 102 | 103 | .reset-button { 104 | background: #333; 105 | border-color: #555; 106 | color: white; 107 | } 108 | 109 | .reset-button:hover { 110 | background: #444; 111 | } 112 | 113 | .reset-button:active { 114 | background: #555; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Spam Scores", 4 | "description": "Name of the add-on" 5 | }, 6 | "extensionDescription": { 7 | "message": "Displays spam scores according to mail headers.", 8 | "description": "Description of the add-on" 9 | }, 10 | "optionsIconRanges": { 11 | "message": "Score icon ranges" 12 | }, 13 | "optionsScoreGreater": { 14 | "message": "Score geater than" 15 | }, 16 | "optionsScoreBetween": { 17 | "message": "Score between $1 and $2 (both inclusive)" 18 | }, 19 | "optionsScoreLess": { 20 | "message": "Score less than" 21 | }, 22 | "optionsHideIconAndScore": { 23 | "message": "Hide icon and score in column" 24 | }, 25 | "optionsHeaderPriority": { 26 | "message": "Header parsing priority" 27 | }, 28 | "optionsHeaderPriorityDescription": { 29 | "message": "Set the order in which spam headers are parsed. The first matching header will be used. Drag to reorder." 30 | }, 31 | "optionsScoreHeaders": { 32 | "message": "Score headers (used for score value)" 33 | }, 34 | "optionsScoreDetailsHeaders": { 35 | "message": "Score details headers (used for score breakdown)" 36 | }, 37 | "optionsResetToDefault": { 38 | "message": "Reset to default order" 39 | }, 40 | "helloPopupTitle": { 41 | "message": "Spam Scores - Thank you!" 42 | }, 43 | "helloHeadline": { 44 | "message": "Thank you for installing Spam Scores!" 45 | }, 46 | "helloParagraph1": { 47 | "message": "In order to display the spam score column:" 48 | }, 49 | "helloInstruction_1": { 50 | "message": "Restart Thunderbird." 51 | }, 52 | "helloInstruction_2": { 53 | "message": "Right-click on a folder and select \"Properties\"." 54 | }, 55 | "helloInstruction_3": { 56 | "message": "Click on \"Repair Folder\"." 57 | }, 58 | "helloParagraph2": { 59 | "message": "This will scan the mail headers of all mails in this folder so that the spam score column can be displayed correctly." 60 | }, 61 | "helloParagraph3": { 62 | "message": "Repair all folders like this in which you want to display the spam column." 63 | }, 64 | "helloParagraph4": { 65 | "message": "Finally, you can enable the \"Spam score\" column in the list view of the repaired folder." 66 | }, 67 | "helloFooterParagraph1": { 68 | "message": "If you have questions or encounter any bugs while using this add-on, please do not hesitate to create an issue for it in on GitHub (https://github.com/friedPotat0/Spam-Scores)." 69 | }, 70 | "helloFooterParagraph2": { 71 | "message": "Please leave a review if you like this add-on. 😊" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /_locales/es/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Spam Scores", 4 | "description": "Nombre de la extensión" 5 | }, 6 | "extensionDescription": { 7 | "message": "Muestra los puntos de Spam según las cabeceras.", 8 | "description": "Descripción de la extensión" 9 | }, 10 | "optionsIconRanges": { 11 | "message": "Rangos de los iconos" 12 | }, 13 | "optionsScoreGreater": { 14 | "message": "Puntuación mayor que" 15 | }, 16 | "optionsScoreBetween": { 17 | "message": "Puntuación entre $1 y $2 (ambos incluidos)" 18 | }, 19 | "optionsScoreLess": { 20 | "message": "Puntuación menor que" 21 | }, 22 | "optionsHideIconAndScore": { 23 | "message": "Ocultar icono y puntuación en la columna" 24 | }, 25 | "optionsHeaderPriority": { 26 | "message": "Prioridad de análisis de encabezados" 27 | }, 28 | "optionsHeaderPriorityDescription": { 29 | "message": "Establezca el orden en que se analizan los encabezados de spam. Se utilizará el primer encabezado coincidente. Arrastre para reordenar." 30 | }, 31 | "optionsScoreHeaders": { 32 | "message": "Encabezados de puntuación (usados para valor de puntuación)" 33 | }, 34 | "optionsScoreDetailsHeaders": { 35 | "message": "Encabezados de detalles de puntuación (usados para desglose)" 36 | }, 37 | "optionsResetToDefault": { 38 | "message": "Restablecer al orden predeterminado" 39 | }, 40 | "helloPopupTitle": { 41 | "message": "Spam Scores - ¡Gracias!" 42 | }, 43 | "helloHeadline": { 44 | "message": "¡Gracias por instalar
Spam Scores!" 45 | }, 46 | "helloParagraph1": { 47 | "message": "Para mostrar la columna de Spam:" 48 | }, 49 | "helloInstruction_1": { 50 | "message": "Reinicia Thunderbird." 51 | }, 52 | "helloInstruction_2": { 53 | "message": "Botón derecho en una carpeta > \"Propiedades\"." 54 | }, 55 | "helloInstruction_3": { 56 | "message": "Click en \"Reparar Carpeta\"." 57 | }, 58 | "helloParagraph2": { 59 | "message": "Ahora escaneará todas las cabeceras de los emails para que se muestre el \"Spam Score\"." 60 | }, 61 | "helloParagraph3": { 62 | "message": "Repara las carpetas en los que te interese tener el \"Spam Score\"." 63 | }, 64 | "helloParagraph4": { 65 | "message": "Finalmente, habilita la columna \"Spam score\" en la vista de la carpeta reparada." 66 | }, 67 | "helloFooterParagraph1": { 68 | "message": "Si tienes dudas o encuentras algun fallo, por favor crea un issue en GitHub (https://github.com/friedPotat0/Spam-Scores)." 69 | }, 70 | "helloFooterParagraph2": { 71 | "message": "Por favor deja una reseña si te gusta esta extensión. 😊" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Spam Scores", 4 | "description": "Name der Erweiterung" 5 | }, 6 | "extensionDescription": { 7 | "message": "Zeigt Spam-Bewertungen anhand von E-Mail-Headern an.", 8 | "description": "Beschreibung der Erweiterung" 9 | }, 10 | "optionsIconRanges": { 11 | "message": "Angezeigte Bewertungssymbole" 12 | }, 13 | "optionsScoreGreater": { 14 | "message": "Punktzahl größer" 15 | }, 16 | "optionsScoreBetween": { 17 | "message": "Punktzahl zwischen $1 und $2 (inklusiv)" 18 | }, 19 | "optionsScoreLess": { 20 | "message": "Punktzahl kleiner" 21 | }, 22 | "optionsHideIconAndScore": { 23 | "message": "Symbol und Punktzahl in der Spalte nicht anzeigen" 24 | }, 25 | "optionsHeaderPriority": { 26 | "message": "Header-Parsing-Priorität" 27 | }, 28 | "optionsHeaderPriorityDescription": { 29 | "message": "Legen Sie die Reihenfolge fest, in der Spam-Header analysiert werden. Der erste übereinstimmende Header wird verwendet. Ziehen Sie zum Neuordnen." 30 | }, 31 | "optionsScoreHeaders": { 32 | "message": "Score-Header (für Gesamtpunktzahl verwendet)" 33 | }, 34 | "optionsScoreDetailsHeaders": { 35 | "message": "Score-Details-Header (für Spam-Details verwendet)" 36 | }, 37 | "optionsResetToDefault": { 38 | "message": "Auf Standardreihenfolge zurücksetzen" 39 | }, 40 | "helloPopupTitle": { 41 | "message": "Spam Scores - Danke!" 42 | }, 43 | "helloHeadline": { 44 | "message": "Vielen Dank, dass Sie Spam Scores installiert haben!" 45 | }, 46 | "helloParagraph1": { 47 | "message": "Um die Spam-Score-Spalte anzuzeigen:" 48 | }, 49 | "helloInstruction_1": { 50 | "message": "Müssen Sie zunächst Thunderbird neu starten." 51 | }, 52 | "helloInstruction_2": { 53 | "message": "Klicken Sie anschließend mit der rechten Maustaste auf einen Ordner und wählen Sie \"Eigenschaften\"." 54 | }, 55 | "helloInstruction_3": { 56 | "message": "Klicken Sie dann auf \"Reparieren\"." 57 | }, 58 | "helloParagraph2": { 59 | "message": "Dadurch werden die Mail-Header aller Mails in diesem Ordner gescannt, damit die Spam-Score-Spalte korrekt angezeigt werden kann." 60 | }, 61 | "helloParagraph3": { 62 | "message": "Reparieren Sie alle Ordner, in denen Sie die Spalte anzeigen möchten." 63 | }, 64 | "helloParagraph4": { 65 | "message": "Anschließend können Sie die Spalte \"Spam score\" in der Listenansicht des reparierten Ordners aktivieren." 66 | }, 67 | "helloFooterParagraph1": { 68 | "message": "Bei Fragen oder Fehlern, zögern Sie bitte nicht, ein Issue dafür auf GitHub zu erstellen (https://github.com/friedPotat0/Spam-Scores)." 69 | }, 70 | "helloFooterParagraph2": { 71 | "message": "Wenn Ihnen das Add-on gefällt, würde ich mich über eine Bewertung sehr freuen. 😊" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /_locales/it/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Spam Scores", 4 | "description": "Name of the add-on" 5 | }, 6 | "extensionDescription": { 7 | "message": "Visualizza i punteggi spam in base alle intestazioni delle email.", 8 | "description": "Description of the add-on" 9 | }, 10 | "optionsIconRanges": { 11 | "message": "Intervalli delle icone di punteggio" 12 | }, 13 | "optionsScoreGreater": { 14 | "message": "Punteggio maggiore di" 15 | }, 16 | "optionsScoreBetween": { 17 | "message": "Punteggio tra $1 e $2 (inclusi entrambi)" 18 | }, 19 | "optionsScoreLess": { 20 | "message": "Punteggio inferiore a" 21 | }, 22 | "optionsHideIconAndScore": { 23 | "message": "Nascondi icona e punteggio nella colonna" 24 | }, 25 | "optionsHeaderPriority": { 26 | "message": "Priorità di analisi degli header" 27 | }, 28 | "optionsHeaderPriorityDescription": { 29 | "message": "Imposta l'ordine in cui vengono analizzati gli header spam. Verrà utilizzato il primo header corrispondente. Trascina per riordinare." 30 | }, 31 | "optionsScoreHeaders": { 32 | "message": "Header di punteggio (utilizzati per il valore del punteggio)" 33 | }, 34 | "optionsScoreDetailsHeaders": { 35 | "message": "Header di dettagli del punteggio (utilizzati per la suddivisione)" 36 | }, 37 | "optionsResetToDefault": { 38 | "message": "Ripristina l'ordine predefinito" 39 | }, 40 | "helloPopupTitle": { 41 | "message": "Spam Scores - Grazie!" 42 | }, 43 | "helloHeadline": { 44 | "message": "Grazie per aver installato Spam Scores!" 45 | }, 46 | "helloParagraph1": { 47 | "message": "Per visualizzare la colonna del punteggio spam:" 48 | }, 49 | "helloInstruction_1": { 50 | "message": "Riavvia Thunderbird." 51 | }, 52 | "helloInstruction_2": { 53 | "message": "Fai clic destro su una cartella e seleziona \"Proprietà\"." 54 | }, 55 | "helloInstruction_3": { 56 | "message": "Clicca su \"Ripara cartella\"." 57 | }, 58 | "helloParagraph2": { 59 | "message": "Questo scansionerà le intestazioni delle email in questa cartella in modo che la colonna del punteggio spam possa essere visualizzata correttamente." 60 | }, 61 | "helloParagraph3": { 62 | "message": "Ripara tutte le cartelle in cui desideri visualizzare la colonna spam in questo modo." 63 | }, 64 | "helloParagraph4": { 65 | "message": "Infine, puoi abilitare la colonna \"Spam Scores\" nella visualizzazione elenco della cartella riparata." 66 | }, 67 | "helloFooterParagraph1": { 68 | "message": "Se hai domande o riscontri errori durante l'utilizzo di questo componente aggiuntivo, non esitare a creare una issue su GitHub (https://github.com/friedPotat0/Spam-Scores)." 69 | }, 70 | "helloFooterParagraph2": { 71 | "message": "Lascia una recensione se ti piace questo componente aggiuntivo. 😊" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | options.html 6 | 7 | 8 | 9 | 10 |

11 |
12 | 13 | 14 | 23 |
24 |
25 | 26 | 27 |
28 |
29 | 30 | 31 |
32 |
33 | 34 | 35 |
36 |
37 | 38 | 39 | 48 |
49 |
50 | 51 | 52 |
53 | 54 |

55 |

56 | 57 |

58 |
59 | 65 | 66 |

67 |
68 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/experiments/schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "namespace": "SpamScores", 4 | "functions": [ 5 | { 6 | "name": "setScoreBounds", 7 | "type": "function", 8 | "description": "", 9 | "async": false, 10 | "parameters": [ 11 | { 12 | "name": "lower", 13 | "type": "number" 14 | }, 15 | { 16 | "name": "upper", 17 | "type": "number" 18 | } 19 | ] 20 | }, 21 | { 22 | "name": "setHideIconScoreOptions", 23 | "type": "function", 24 | "description": "", 25 | "async": false, 26 | "parameters": [ 27 | { 28 | "name": "hidePositive", 29 | "type": "boolean" 30 | }, 31 | { 32 | "name": "hideNeutral", 33 | "type": "boolean" 34 | }, 35 | { 36 | "name": "hideNegative", 37 | "type": "boolean" 38 | } 39 | ] 40 | }, 41 | { 42 | "name": "getHelloFlag", 43 | "type": "function", 44 | "description": "", 45 | "async": true, 46 | "parameters": [] 47 | }, 48 | { 49 | "name": "setCustomMailscannerHeaders", 50 | "type": "function", 51 | "description": "", 52 | "async": false, 53 | "parameters": [ 54 | { 55 | "name": "customMailscannerHeaders", 56 | "type": "array", 57 | "items": { 58 | "type": "string" 59 | } 60 | } 61 | ] 62 | }, 63 | { 64 | "name": "setScoreHeaderOrder", 65 | "type": "function", 66 | "description": "Set the order in which score headers are parsed", 67 | "async": false, 68 | "parameters": [ 69 | { 70 | "name": "scoreHeaderOrder", 71 | "type": "array", 72 | "items": { 73 | "type": "string" 74 | } 75 | } 76 | ] 77 | }, 78 | { 79 | "name": "setScoreDetailsHeaderOrder", 80 | "type": "function", 81 | "description": "Set the order in which score details headers are parsed", 82 | "async": false, 83 | "parameters": [ 84 | { 85 | "name": "scoreDetailsHeaderOrder", 86 | "type": "array", 87 | "items": { 88 | "type": "string" 89 | } 90 | } 91 | ] 92 | }, 93 | { 94 | "name": "addHeadersToPrefs", 95 | "type": "function", 96 | "description": "", 97 | "async": true, 98 | "parameters": [ 99 | { 100 | "name": "dynamicHeaders", 101 | "type": "array", 102 | "optional": true, 103 | "items": { 104 | "type": "string" 105 | } 106 | } 107 | ] 108 | }, 109 | { 110 | "name": "addColumns", 111 | "type": "function", 112 | "description": "", 113 | "async": true, 114 | "parameters": [ 115 | { 116 | "name": "nameSpamCoreValue", 117 | "type": "string" 118 | }, 119 | { 120 | "name": "nameSpamCoreIcon", 121 | "type": "string" 122 | } 123 | ] 124 | }, 125 | { 126 | "name": "removeColumns", 127 | "type": "function", 128 | "description": "", 129 | "async": true, 130 | "parameters": [] 131 | } 132 | ] 133 | } 134 | ] 135 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Constants module 3 | * @module constants 4 | * @see module:functions 5 | */ 6 | 7 | /** 8 | * A score lower than this is considered a good email 9 | * @constant {number} 10 | */ 11 | export const DEFAULT_SCORE_LOWER_BOUNDS = -2.0 12 | 13 | /** 14 | * A score higher than this is considered a bad email 15 | * @constant {number} 16 | */ 17 | export const DEFAULT_SCORE_UPPER_BOUNDS = 2.0 18 | 19 | // /** 20 | // * Minimum score for SCORE_INTERPOLATION 21 | // * @constant {number} Minimum score seen from the first Score Domain that was made for (Rspamd score) 22 | // * @see module:functions.scoreInterpolation 23 | // */ 24 | // export const MIN_SCORE_SEEN = -40 25 | 26 | // /** 27 | // * Maximum score for SCORE_INTERPOLATION 28 | // * @constant {number} Maximum score seen from the first Score Domain that was made for (Rspamd score) 29 | // * @see module:functions.scoreInterpolation 30 | // */ 31 | // export const MAX_SCORE_SEEN = 40 32 | 33 | // /** 34 | // * @constant {Object} 35 | // * @type {Object} 36 | // */ 37 | // export const SCORE_INTERPOLATION = { 38 | // 'x-vr-spamscore': { MIN_VALUE: 0, MAX_VALUE: 900, LOWER_BOUNDS: 100, UPPER_BOUNDS: 300 } 39 | // } 40 | 41 | /** 42 | * @constant {Object} 43 | */ 44 | export const SCORE_REGEX = { 45 | 'x-spamd-result': /\[([-+]?[0-9]+\.?[0-9]*) \/ [-+]?[0-9]+\.?[0-9]*\];/, 46 | 'x-spam-status': /(?:Yes|No)(?:, score=|\/)([-+]?[0-9]+\.?[0-9]*)/, 47 | 'x-rspam-status': /(?:Yes|No)(?:, score=|\/)([-+]?[0-9]+\.?[0-9]*)/, 48 | 'x-spam-score': /([-+]?[0-9]+\.?[0-9]*)/, 49 | 'x-spam-report': /([-+]?[0-9]+\.?[0-9]*) hits,/, 50 | 'x-ham-report': /([-+]?[0-9]+\.?[0-9]*) hits,/, 51 | 'x-rspamd-score': /([-+]?[0-9]+\.?[0-9]*)/, 52 | 'x-vr-spamscore': /([0-9]+)/, 53 | 'x-hmailserver-reason-score': /([-+]?[0-9]+\.?[0-9]*)/ 54 | } 55 | 56 | /** 57 | * For customised headers 58 | * @constant {Object} 59 | */ 60 | export const CUSTOM_SCORE_REGEX = { 61 | 'mailscanner-spamcheck': 62 | /(?:score|punteggio|puntuació|sgor\/score|skore|Wertung|bedømmelse|puntaje|pont|escore|resultat|skore)=([-+]?[0-9]+\.?[0-9]*),/ 63 | } 64 | 65 | /** 66 | * Regex for hMailServer reason headers 67 | * @constant {RegExp} 68 | */ 69 | export const HMAILSERVER_REASON_REGEX = /^x-hmailserver-reason-\d+$/ 70 | 71 | /** 72 | * Headers that contain the score details 73 | * @constant {String[]} 74 | */ 75 | export const SCORE_DETAILS_ARRAY = [ 76 | 'x-spamd-result', 77 | 'x-spam-report', 78 | 'x-ham-report', 79 | 'x-spamcheck', 80 | 'x-spam-status', 81 | 'x-rspamd-report', 82 | 'x-hmailserver-reason-score' 83 | ] 84 | 85 | /** 86 | * Default order for parsing score headers (first match is used) 87 | * @constant {String[]} 88 | */ 89 | export const DEFAULT_SCORE_HEADER_ORDER = [ 90 | 'x-spamd-result', 91 | 'x-spam-status', 92 | 'x-rspam-status', 93 | 'x-spam-score', 94 | 'x-spam-report', 95 | 'x-ham-report', 96 | 'x-rspamd-score', 97 | 'x-vr-spamscore', 98 | 'x-hmailserver-reason-score' 99 | ] 100 | 101 | /** 102 | * Default order for parsing score details headers (first match is used) 103 | * @constant {String[]} 104 | */ 105 | export const DEFAULT_SCORE_DETAILS_ORDER = [ 106 | 'x-spamd-result', 107 | 'x-spam-report', 108 | 'x-ham-report', 109 | 'x-spam-status', 110 | 'x-rspamd-report', 111 | 'x-hmailserver-reason-score' 112 | ] 113 | 114 | /** @constant {Object} */ 115 | export const SYMBOL_REGEX = { 116 | prefix: /\*? +-?[\d.]+[ \)=]+(?:[A-Z][A-Z0-9_]+|--) .*?(?=\*? +-?[\d.]+[ \)=]+(?:[A-Z][A-Z0-9_]+|--) |$)/gs, 117 | prefixSingle: /(?:\* +)?(-?[\d.]+)[ \)=]+(?:([A-Z][A-Z0-9_]+)|--) ([\s\S]*?)(?:\[(.*)\])?$/, 118 | suffix: /([A-Z][A-Z0-9_]+)(?:(?:[ \(=](-?[\d.]+)\)?(?:\[(.*?)\])?)|, *| |\r?\n|$)/g 119 | } 120 | -------------------------------------------------------------------------------- /test/mail-examples/spam-status-8.eml: -------------------------------------------------------------------------------- 1 | To: receiver@spamscores 2 | From: sender@spamscores 3 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-8.eml) 4 | X-Spam-Subject: [X-Spam-Status] Spam Scores test mail (spam-status-8.eml) 5 | X-Spam-Status: Yes, score=-42.42 6 | X-Spam-Score: -4242 7 | X-Spam-Bar: +++++++++++++++++++++ 8 | X-Spam-Report: Spam detection software, running on the system "*****", 9 | has identified this incoming email as possible spam. The original 10 | message has been attached to this so you can view it or label 11 | similar future email. If you have any questions, see 12 | root\@localhost for details. 13 | Content preview: This is a message to test the Spam Scores Thunderbird add-on. [...] 14 | Content analysis details: (21.2 points, 5.0 required) 15 | pts rule name description 16 | ---- ---------------------- -------------------------------------------------- 17 | 4.5 URIBL_DBL_SPAM Contains a spam URL listed in the Spamhaus DBL 18 | blocklist 19 | [URIs: topdelivery.net.pl] 20 | 5.0 URIBL_BLACK Contains an URL listed in the URIBL blacklist 21 | [URIs: topdelivery.net.pl] 22 | 1.0 BAYES_999 BODY: Bayes spam probability is 99.9 to 100% 23 | [score: 1.0000] 24 | 5.0 BAYES_99 BODY: Bayes spam probability is 99 to 100% 25 | [score: 1.0000] 26 | 0.2 HEADER_FROM_DIFFERENT_DOMAINS From and EnvelopeFrom 2nd level 27 | mail domains are different 28 | -0.0 SPF_PASS SPF: sender matches SPF record 29 | 0.0 HTML_MESSAGE BODY: HTML included in message 30 | 0.0 HTML_IMAGE_RATIO_02 BODY: HTML has a low ratio of text to image 31 | area 32 | 0.5 KAM_REALLYHUGEIMGSRC RAW: Spam with image tags with ridiculously 33 | huge http urls 34 | -0.1 DKIM_VALID Message has at least one valid DKIM or DK signature 35 | 0.1 DKIM_SIGNED Message has a DKIM or DK signature, not necessarily 36 | valid 37 | -0.1 DKIM_VALID_EF Message has a valid DKIM or DK signature from 38 | envelope-from domain 39 | 5.0 KAM_VERY_BLACK_DBL Email that hits both URIBL Black and Spamhaus 40 | DBL 41 | X-Spam-Flag: YES 42 | 43 | This is a message to test the Spam Scores Thunderbird add-on. 44 | 45 | -- 46 | To: receiver@spamscores 47 | From: sender@spamscores 48 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-8.eml) 49 | X-Spam-Subject: [X-Spam-Status] Spam Scores test mail (spam-status-8.eml) 50 | X-Spam-Status: Yes, score=-42.42 51 | X-Spam-Score: -4242 52 | X-Spam-Bar: +++++++++++++++++++++ 53 | X-Spam-Report: Spam detection software, running on the system "*****", 54 | has identified this incoming email as possible spam. The original 55 | message has been attached to this so you can view it or label 56 | similar future email. If you have any questions, see 57 | root\@localhost for details. 58 | Content preview: This is a message to test the Spam Scores Thunderbird add-on. [...] 59 | Content analysis details: (21.2 points, 5.0 required) 60 | pts rule name description 61 | ---- ---------------------- -------------------------------------------------- 62 | 4.5 URIBL_DBL_SPAM Contains a spam URL listed in the Spamhaus DBL 63 | blocklist 64 | [URIs: topdelivery.net.pl] 65 | 5.0 URIBL_BLACK Contains an URL listed in the URIBL blacklist 66 | [URIs: topdelivery.net.pl] 67 | 1.0 BAYES_999 BODY: Bayes spam probability is 99.9 to 100% 68 | [score: 1.0000] 69 | 5.0 BAYES_99 BODY: Bayes spam probability is 99 to 100% 70 | [score: 1.0000] 71 | 0.2 HEADER_FROM_DIFFERENT_DOMAINS From and EnvelopeFrom 2nd level 72 | mail domains are different 73 | -0.0 SPF_PASS SPF: sender matches SPF record 74 | 0.0 HTML_MESSAGE BODY: HTML included in message 75 | 0.0 HTML_IMAGE_RATIO_02 BODY: HTML has a low ratio of text to image 76 | area 77 | 0.5 KAM_REALLYHUGEIMGSRC RAW: Spam with image tags with ridiculously 78 | huge http urls 79 | -0.1 DKIM_VALID Message has at least one valid DKIM or DK signature 80 | 0.1 DKIM_SIGNED Message has a DKIM or DK signature, not necessarily 81 | valid 82 | -0.1 DKIM_VALID_EF Message has a valid DKIM or DK signature from 83 | envelope-from domain 84 | 5.0 KAM_VERY_BLACK_DBL Email that hits both URIBL Black and Spamhaus 85 | DBL 86 | X-Spam-Flag: YES -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spam Scores (Thunderbird Add-on) 2 | 3 | Spam Scores is an add-on for Thunderbird (Version 91.0a1 - \*). For Thunderbird 78.* the last working version of this add-on is [1.3.1](https://github.com/friedPotat0/Spam-Scores/releases/tag/1.3.1). 4 | 5 | The add-on can display spam scores according to mail headers. The add-on supports spam/ham score headers of Rspamd, SpamAssassin, MailScanner and hMailServer. It adds a column with the overall spam score to the mail list view and shows details of any matched spam/ham rule. 6 | 7 | > :warning: The add-on needs mails with headers like "X-Spamd-Result", "X-Spam-Report", "X-Ham-Report", "X-Rspamd-Report/-Score", "X-SpamCheck", "X-Spam-Status", "X-Rspam-Status", "X-hMailServer-Reason-Score" or "X-VR-SPAMSCORE" to work. If a mail does not have one of these headers, it cannot display any spam score. Please make sure to check your mails for these headers before creating an issue. The add-on does not currently support spam headers from GMX, as these have a different score scale. 8 | 9 | ![Add-on Screenshot](https://raw.githubusercontent.com/friedPotat0/Spam-Scores/master/images/screenshot.jpg) 10 | 11 | To display the spam score column, right-click on the title bar of the columns in the list view and select "Spam score". If the column is empty, you must first restart Thunderbird and then right-click on any folder and select "Properties" and "Repair Folder". This will scan the mail headers of all mails in this folder so that the spam score column can be displayed correctly. Repair all folders like this in which you want to display this column. 12 | 13 | If you have mails with the header "X-MYCOMPANY-MailScanner-SpamCheck", you have to open one of these mails first and then restart Thunderbird and repair the folder. Otherwise the spam score of the mails containing these headers will not be displayed. 14 | 15 | The total score of each mail with an existing spam header will be displayed along with a red, yellow or green icon depending on the score. The colours are by default calculated as follows: 16 | 17 | - ![Positive Score](https://raw.githubusercontent.com/friedPotat0/Spam-Scores/master/images/score_positive.png) Score greater than 2 18 | - ![Neutral Score](https://raw.githubusercontent.com/friedPotat0/Spam-Scores/master/images/score_neutral.png) Score between -2 and 2 (both inclusive) 19 | - ![Negative Score](https://raw.githubusercontent.com/friedPotat0/Spam-Scores/master/images/score_negative.png) Score less than -2 20 | 21 | The icon score ranges can be changed in the [add-on options](#options). 22 | 23 | Furthermore, a button is displayed in the action bar of any opened mail with the respective total score of the mail. Clicking on the button opens a popup with detailed information on all individual rules. In addition to the name and the partial score, a description and, if available in the mail header, the value on the basis of which the score was calculated is displayed. 24 | 25 | ## Installation 26 | 27 | You can download the latested version reviewed by moz://a directly on the [Thunderbird Add-on page](https://addons.thunderbird.net/de/thunderbird/addon/spam-scores/) or through your installed Thunderbird client by clicking on the menu button followed by "Add-ons" and typing "Spam Scores" in the search bar. 28 | 29 | Additionally the latest reviewed version is available on the [Releases page](https://github.com/friedPotat0/Spam-Scores/releases) of this GitHub repository. 30 | 31 | To test versions that have not yet been published, you can always download the files from any branch and create a new ZIP file containing all files in the "Spam-Scores-[BRANCH_NAME]" folder. Then you can add the file to thunderbird by drag & drop to install the new version. Please keep in mind that you might not receive future updates until you reinstall a reviewed version directly from Thunderbird's add-on page or through the releases page of this repository. 32 | 33 | ## Options 34 | 35 | ![Settings Screenshot](https://raw.githubusercontent.com/friedPotat0/Spam-Scores/master/images/screenshot_settings.jpg) 36 | 37 | The default icon ranges can be changed in the add-on settings in Thunderbird. Furthermore individual score ranges can be completely hidden. For example, this can be used to show the icon and score only for mails that are spam. The detailed rules of any opened mail can still be accessed regardless of this setting. 38 | 39 | ## Translations 40 | 41 | At the moment the add-on is mostly written in English. Some parts like the settings and the description are also translated to German. Please refer to the section [Contributing](#contributing) if you would like to help by translating the add-on to different languages. 42 | 43 | ## License 44 | 45 | The add-on is released under the CC BY-NC-SA 4.0 (Attribution-NonCommercial-ShareAlike 4.0 International) license. 46 | 47 | ## Contributing 48 | 49 | If you notice any bugs, do not hesitate to open an issue about it. Please understand that I develop the add-on in my spare time and may not be able to solve problems directly. If you want to contribute to the project by fixing bugs, implementing new features or translating the add-on, please feel free to open a pull request. 50 | -------------------------------------------------------------------------------- /test/mail-examples/spam-report-1.eml: -------------------------------------------------------------------------------- 1 | To: receiver@spamscores 2 | From: sender@spamscores 3 | Subject: [X-Spam-Report] Spam Scores test mail (spam-report-1.eml) 4 | X-Spam-Subject: ***SPAM*** =?utf-8?B?TmllIHByemVnYXAhIE5vd2/Fm2NpIC0yNSUgeiBrb2RlbSBGUkVTSDI1IQ==?= 5 | X-Spam-Status: Yes, score=-42.42 6 | X-Spam-Score: -4242 7 | X-Spam-Bar: +++++++++++++ 8 | X-Spam-Report: Spam detection software, running on the system "xxxxx", 9 | has identified this incoming email as possible spam. The original 10 | message has been attached to this so you can view it or label 11 | similar future email. If you have any questions, see 12 | root\@localhost for details. 13 | Content preview: SprawdĹşsporstylestory_mailing-sale DARMOWA DOSTAWA OD 200 14 | PLN Â Â / Â Â 30 DNI NA ZWROT Â Â / Â Â BEZPIECZNE ZAKUPY Â Â / Â Â 15 | RATY 0%Kampania realizowana przez Redgroup do bazy partnera Kampani [...] 16 | 17 | Content analysis details: (13.3 points, 4.0 required) 18 | pts rule name description 19 | ---- ---------------------- -------------------------------------------------- 20 | 4.5 URIBL_DBL_SPAM Contains a spam URL listed in the Spamhaus DBL 21 | blocklist 22 | [URIs: topdelivery.net.pl] 23 | -1.9 BAYES_00 BODY: Bayes spam probability is 0 to 1% 24 | [score: 0.0000] 25 | 5.0 URIBL_BLACK Contains an URL listed in the URIBL blacklist 26 | [URIs: topdelivery.net.pl] 27 | -0.0 SPF_PASS SPF: sender matches SPF record 28 | 0.2 HEADER_FROM_DIFFERENT_DOMAINS From and EnvelopeFrom 2nd level 29 | mail domains are different 30 | 0.0 HTML_FONT_LOW_CONTRAST BODY: HTML font color similar or 31 | identical to background 32 | 0.0 HTML_IMAGE_RATIO_02 BODY: HTML has a low ratio of text to image 33 | area 34 | 0.0 HTML_MESSAGE BODY: HTML included in message 35 | 0.5 KAM_REALLYHUGEIMGSRC RAW: Spam with image tags with ridiculously 36 | huge http urls 37 | -0.1 DKIM_VALID Message has at least one valid DKIM or DK signature 38 | 0.1 DKIM_SIGNED Message has a DKIM or DK signature, not necessarily 39 | valid 40 | -0.1 DKIM_VALID_EF Message has a valid DKIM or DK signature from 41 | envelope-from domain 42 | 5.0 KAM_VERY_BLACK_DBL Email that hits both URIBL Black and Spamhaus 43 | DBL 44 | X-Spam-Flag: YES 45 | 46 | This is a message to test the Spam Scores Thunderbird add-on. 47 | 48 | -- 49 | To: receiver@spamscores 50 | From: sender@spamscores 51 | Subject: [X-Spam-Report] Spam Scores test mail (spam-report-1.eml) 52 | X-Spam-Subject: ***SPAM*** =?utf-8?B?TmllIHByemVnYXAhIE5vd2/Fm2NpIC0yNSUgeiBrb2RlbSBGUkVTSDI1IQ==?= 53 | X-Spam-Status: Yes, score=-42.42 54 | X-Spam-Score: -4242 55 | X-Spam-Bar: +++++++++++++ 56 | X-Spam-Report: Spam detection software, running on the system "xxxxx", 57 | has identified this incoming email as possible spam. The original 58 | message has been attached to this so you can view it or label 59 | similar future email. If you have any questions, see 60 | root\@localhost for details. 61 | Content preview: SprawdĹşsporstylestory_mailing-sale DARMOWA DOSTAWA OD 200 62 | PLN Â Â / Â Â 30 DNI NA ZWROT Â Â / Â Â BEZPIECZNE ZAKUPY Â Â / Â Â 63 | RATY 0%Kampania realizowana przez Redgroup do bazy partnera Kampani [...] 64 | 65 | Content analysis details: (13.3 points, 4.0 required) 66 | pts rule name description 67 | ---- ---------------------- -------------------------------------------------- 68 | 4.5 URIBL_DBL_SPAM Contains a spam URL listed in the Spamhaus DBL 69 | blocklist 70 | [URIs: topdelivery.net.pl] 71 | -1.9 BAYES_00 BODY: Bayes spam probability is 0 to 1% 72 | [score: 0.0000] 73 | 5.0 URIBL_BLACK Contains an URL listed in the URIBL blacklist 74 | [URIs: topdelivery.net.pl] 75 | -0.0 SPF_PASS SPF: sender matches SPF record 76 | 0.2 HEADER_FROM_DIFFERENT_DOMAINS From and EnvelopeFrom 2nd level 77 | mail domains are different 78 | 0.0 HTML_FONT_LOW_CONTRAST BODY: HTML font color similar or 79 | identical to background 80 | 0.0 HTML_IMAGE_RATIO_02 BODY: HTML has a low ratio of text to image 81 | area 82 | 0.0 HTML_MESSAGE BODY: HTML included in message 83 | 0.5 KAM_REALLYHUGEIMGSRC RAW: Spam with image tags with ridiculously 84 | huge http urls 85 | -0.1 DKIM_VALID Message has at least one valid DKIM or DK signature 86 | 0.1 DKIM_SIGNED Message has a DKIM or DK signature, not necessarily 87 | valid 88 | -0.1 DKIM_VALID_EF Message has a valid DKIM or DK signature from 89 | envelope-from domain 90 | 5.0 KAM_VERY_BLACK_DBL Email that hits both URIBL Black and Spamhaus 91 | DBL 92 | X-Spam-Flag: YES -------------------------------------------------------------------------------- /src/background/background.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import { SCORE_REGEX, CUSTOM_SCORE_REGEX, DEFAULT_SCORE_HEADER_ORDER } from '../constants.js' 3 | import { getBounds /* , scoreInterpolation */ } from '../functions.js' 4 | 5 | /** 6 | * @type {StorageArea} 7 | */ 8 | const localStorage = messenger.storage.local 9 | 10 | /** 11 | * Functions 12 | */ 13 | 14 | /** 15 | * @param {object} headers 16 | * @param {string[]} headerOrder - Custom order for parsing headers 17 | * @returns {string[]} Score value 18 | */ 19 | function getScores(headers, headerOrder = null) { 20 | const scores = [] 21 | // Get Custom Mail Headers 22 | const auxHeaders = Object.entries(headers).filter(([key, value]) => key.startsWith('x-')) 23 | // Remove Mozilla Headers 24 | const auxHeadersNoMozilla = auxHeaders.filter(([key, value]) => !key.startsWith('x-mozilla')) 25 | const customHeaders = Object.fromEntries(auxHeadersNoMozilla) 26 | 27 | // Use custom order if provided, otherwise use default order 28 | const scoreHeaders = headerOrder || DEFAULT_SCORE_HEADER_ORDER 29 | 30 | for (const headerName of scoreHeaders) { 31 | if (customHeaders[headerName]) { 32 | if (SCORE_REGEX[headerName]) { 33 | const scoreField = customHeaders[headerName][0].match(SCORE_REGEX[headerName]) 34 | if (!scoreField) continue // If no match iterate 35 | // const score = scoreInterpolation(headerName, scoreField[1]) 36 | const score = scoreField[1] 37 | scores.push(score) 38 | } 39 | } 40 | } 41 | 42 | // Check custom headers (e.g., mailscanner) if no score found yet 43 | if (scores.length === 0) { 44 | for (const headerName in customHeaders) { 45 | for (const regExName in CUSTOM_SCORE_REGEX) { 46 | if (headerName.endsWith(regExName)) { 47 | const scoreField = customHeaders[headerName][0].match(CUSTOM_SCORE_REGEX[regExName]) 48 | if (!scoreField) continue // If no match iterate 49 | // const score = scoreInterpolation(headerName, scoreField[1]) 50 | const score = scoreField[1] 51 | scores.push(score) 52 | } 53 | } 54 | } 55 | } 56 | 57 | return scores 58 | } 59 | 60 | /** 61 | * Returns the path of the image 62 | * @param {string} score 63 | * @returns {string} Path of Image 64 | */ 65 | async function getImageSrc(score) { 66 | if (score === null) return '/images/score_no.svg' 67 | const storage = await localStorage.get(['scoreIconLowerBounds', 'scoreIconUpperBounds']) 68 | const [lowerBounds, upperBounds] = getBounds(storage) 69 | if (score > upperBounds) return '/images/score_positive.svg' 70 | if (score <= upperBounds && score >= lowerBounds) return '/images/score_neutral.svg' 71 | if (score < lowerBounds) return '/images/score_negative.svg' 72 | return '/images/score_neutral.svg' 73 | } 74 | 75 | /** 76 | * Executed everytime a message is displayed 77 | * @param {Tab} tab 78 | * @param {MessageHeader} message 79 | */ 80 | async function onMessageDisplayed(tab, message) { 81 | // Declaration / Values 82 | const idTab = tab.id 83 | const fullMessage = await messenger.messages.getFull(message.id) 84 | const messageButton = messenger.messageDisplayAction 85 | 86 | // Get custom header order 87 | const storage = await localStorage.get(['scoreHeaderOrder']) 88 | const headerOrder = storage.scoreHeaderOrder || DEFAULT_SCORE_HEADER_ORDER 89 | 90 | // Get Score 91 | const scores = getScores(fullMessage.headers, headerOrder) // Get Scores 92 | const score = isNaN(scores[0]) ? null : scores[0] 93 | 94 | // Message Score Button 95 | if (score === null) { 96 | messageButton.disable(idTab) 97 | messageButton.setTitle({ tabId: idTab, title: 'No Spam Score' }) 98 | messageButton.setIcon({ path: await getImageSrc(null) }) 99 | } else { 100 | messageButton.enable(idTab) 101 | messageButton.setTitle({ tabId: idTab, title: 'Spam Score: ' + score }) 102 | messageButton.setIcon({ path: await getImageSrc(score) }) 103 | } 104 | 105 | /** 106 | * Save static (e.g. x-spam-score, x-rspamd-score) and dynamic (X--MailScanner-SpamCheck) header names 107 | * in global preferences to be stored by Thunderbird for each mail 108 | * 109 | * Reason to restart Thunderbird & repair folders 110 | */ 111 | const dynamicHeaders = [] 112 | let dynamicHeaderFound = false 113 | for (const dynamicHeaderSuffix in CUSTOM_SCORE_REGEX) { 114 | const fullDynamicHeaderName = Object.keys(fullMessage.headers).find(key => key.endsWith(dynamicHeaderSuffix)) 115 | if (fullDynamicHeaderName) { 116 | dynamicHeaders.push(fullDynamicHeaderName) 117 | dynamicHeaderFound = true 118 | } 119 | } 120 | // Static header names will be automatically added 121 | await messenger.SpamScores.addHeadersToPrefs(dynamicHeaders) 122 | // Store new dynamic header names in localStorage to be recognised by the score column 123 | if (dynamicHeaderFound) { 124 | const storage = await localStorage.get(['customMailscannerHeaders']) 125 | let customMailscannerHeaders = storage.customMailscannerHeaders || [] 126 | for (const header of dynamicHeaders) { 127 | if (!customMailscannerHeaders.includes(header)) { 128 | customMailscannerHeaders.push(header) 129 | } 130 | } 131 | localStorage.set({ customMailscannerHeaders }) 132 | } 133 | } 134 | 135 | /** 136 | * Main 137 | */ 138 | const init = async () => { 139 | // Declaration / Values 140 | const spamScores = messenger.SpamScores 141 | const storage = await localStorage.get([ 142 | 'scoreIconLowerBounds', 143 | 'scoreIconUpperBounds', 144 | 'customMailscannerHeaders', 145 | 'hideIconScorePositive', 146 | 'hideIconScoreNeutral', 147 | 'hideIconScoreNegative', 148 | 'scoreHeaderOrder', 149 | 'scoreDetailsHeaderOrder', 150 | 'hello' 151 | ]) 152 | 153 | // Hello Message 154 | if (!storage.hello) { 155 | /** 156 | * Additional condition deprecated (Fallback for add-on version <= 1.3.1) 157 | * Prevents displaying the message on subsequent installations. 158 | * Should be removed when majority has updated! 159 | */ 160 | if (!(await spamScores.getHelloFlag())) { 161 | messenger.windows.create({ 162 | height: 680, 163 | width: 488, 164 | url: '/src/static/hello.html', 165 | type: 'popup' 166 | }) 167 | } 168 | localStorage.set({ hello: true }) 169 | } 170 | 171 | // Add Listeners 172 | messenger.messageDisplay.onMessageDisplayed.addListener(onMessageDisplayed) 173 | 174 | // Init Data 175 | const [lowerBounds, upperBounds] = getBounds(storage) 176 | spamScores.setScoreBounds(lowerBounds, upperBounds) 177 | 178 | if (storage.customMailscannerHeaders) { 179 | spamScores.setCustomMailscannerHeaders(storage.customMailscannerHeaders) 180 | } 181 | 182 | // Set header order preferences 183 | if (storage.scoreHeaderOrder) { 184 | spamScores.setScoreHeaderOrder(storage.scoreHeaderOrder) 185 | } 186 | if (storage.scoreDetailsHeaderOrder) { 187 | spamScores.setScoreDetailsHeaderOrder(storage.scoreDetailsHeaderOrder) 188 | } 189 | 190 | spamScores.setHideIconScoreOptions( 191 | storage.hideIconScorePositive || false, 192 | storage.hideIconScoreNeutral || false, 193 | storage.hideIconScoreNegative || false 194 | ) 195 | spamScores.addColumns('SpamScore', 'SpamScore (Icon)') 196 | } 197 | init() 198 | -------------------------------------------------------------------------------- /src/browser/popup/popup.js: -------------------------------------------------------------------------------- 1 | import { SCORE_SYMBOLS } from './score_symbols.js' 2 | import { 3 | SCORE_DETAILS_ARRAY, 4 | SYMBOL_REGEX, 5 | HMAILSERVER_REASON_REGEX, 6 | DEFAULT_SCORE_DETAILS_ORDER 7 | } from '../../constants.js' 8 | 9 | messenger.tabs 10 | .query({ 11 | active: true, 12 | currentWindow: true 13 | }) 14 | .then(async tabs => { 15 | // Declaration / Values 16 | const tabId = tabs[0].id 17 | messenger.messageDisplay.getDisplayedMessage(tabId).then(async message => { 18 | const fullMessage = await messenger.messages.getFull(message.id) 19 | const allDetailScores = await getParsedDetailScores(fullMessage.headers) 20 | const parsedDetailScores = await deduplicateValues(allDetailScores) 21 | if (parsedDetailScores.length !== 0) { 22 | const groupedDetailScores = { 23 | positive: parsedDetailScores.filter(el => el.score > 0).sort((a, b) => b.score - a.score), 24 | negative: parsedDetailScores.filter(el => el.score < 0).sort((a, b) => a.score - b.score), 25 | neutral: parsedDetailScores.filter(el => el.score === 0).sort((a, b) => a.name.localeCompare(b.name)) 26 | } 27 | let scoreDetailTable = document.querySelector('table#score-details') 28 | scoreDetailTable.style.display = 'block' 29 | const rowTemplate = document.querySelector('template#score-detail-row') 30 | 31 | for (const groupType of ['positive', 'negative', 'neutral']) { 32 | for (const detailElement of groupedDetailScores[groupType]) { 33 | // Get symbol description 34 | const symbolDescription = SCORE_SYMBOLS.find(sym => sym.name === detailElement.name)?.description 35 | 36 | // Clone template row 37 | let detailRow = document.importNode(rowTemplate.content, true) 38 | 39 | // Fill in data 40 | detailRow.querySelector('.score-detail-row').classList.add(groupType) 41 | detailRow.querySelector('.score').textContent = detailElement.score 42 | detailRow.querySelector('.name').textContent = detailElement.name || '-' 43 | if (symbolDescription) { 44 | detailRow.querySelector('.description').textContent = symbolDescription 45 | } else { 46 | detailRow.querySelector('.description').textContent = detailElement.description 47 | } 48 | if (detailElement.info) { 49 | detailRow.querySelector('.info').textContent = `[${detailElement.info}]` 50 | } 51 | 52 | // Add row to table 53 | scoreDetailTable.append(detailRow) 54 | } 55 | } 56 | 57 | // Workaround for a bug where Thunderbird does not correctly calculate the popup window height in Wide View layout (see issue #33) 58 | document.querySelector('body').style.maxHeight = `${window.screen.height / 2 - 60}px` 59 | } else { 60 | document.querySelector('#no-details').style.display = 'block' 61 | } 62 | }) 63 | }) 64 | 65 | /** 66 | * Parse the headers 67 | * @param {Object !scoreDetailsOrder.includes(h))] 80 | 81 | for (const headerName of headersToCheck) { 82 | if (headers[headerName]) { 83 | // Special handling for x-hmailserver-reason-score 84 | if (headerName === 'x-hmailserver-reason-score') { 85 | // Parse hMailServer reason headers 86 | for (const hdrName in headers) { 87 | if (HMAILSERVER_REASON_REGEX.test(hdrName)) { 88 | const headerValue = headers[hdrName][0] 89 | // Format: "Description - (Score: X)" 90 | const match = headerValue.match(/^(.+?)\s*-\s*\(Score:\s*([-+]?[0-9]+\.?[0-9]*)\)/) 91 | if (match) { 92 | const description = match[1].trim() 93 | const score = parseFloat(match[2]) 94 | // Create a simplified name from the description 95 | const name = hdrName.toUpperCase().replace('X-HMAILSERVER-REASON-', 'REASON_') 96 | parsedDetailScores.push({ 97 | name: name, 98 | score: score, 99 | info: '', 100 | description: description 101 | }) 102 | } 103 | } 104 | } 105 | if (parsedDetailScores.length > 0) { 106 | break // Found details, stop looking 107 | } 108 | continue 109 | } 110 | 111 | // Regular header parsing 112 | let headerValue = headers[headerName][0] // For some reason thunderbird always saves it as an array 113 | if (headerName === 'x-spam-report' || headerName === 'x-ham-report') { 114 | const reportSplitted = headerValue.split('Content analysis details:') 115 | if (reportSplitted.length > 1) { 116 | headerValue = reportSplitted[1] 117 | } 118 | } 119 | headerValue = headerValue.trim().replace(/\r?\n/g, ' ') 120 | let symbolMatch = headerValue.match(SYMBOL_REGEX.prefix) 121 | if (symbolMatch && symbolMatch.length > 0) { 122 | const detailScore = symbolMatch.map(el => ({ 123 | name: sanitizeRegexResult(el.replace(SYMBOL_REGEX.prefixSingle, '$2')), 124 | score: parseFloat(sanitizeRegexResult(el.replace(SYMBOL_REGEX.prefixSingle, '$1')) || 0), 125 | info: sanitizeRegexResult(el.replace(SYMBOL_REGEX.prefixSingle, '$4')) || '', 126 | description: sanitizeRegexResult(el.replace(SYMBOL_REGEX.prefixSingle, '$3')) || '' 127 | })) 128 | parsedDetailScores = [...parsedDetailScores, ...detailScore] 129 | // Use first matching header only (like for score headers) 130 | break 131 | } 132 | 133 | symbolMatch = headerValue.match(SYMBOL_REGEX.suffix) 134 | if (symbolMatch && symbolMatch.length > 0) { 135 | const detailScore = symbolMatch.map(el => ({ 136 | name: sanitizeRegexResult(el.replace(SYMBOL_REGEX.suffix, '$1')), 137 | score: parseFloat(sanitizeRegexResult(el.replace(SYMBOL_REGEX.suffix, '$2')) || 0), 138 | info: sanitizeRegexResult(el.replace(SYMBOL_REGEX.suffix, '$3')) || '' 139 | })) 140 | parsedDetailScores = [...parsedDetailScores, ...detailScore] 141 | // Use first matching header only (like for score headers) 142 | break 143 | } 144 | } 145 | } 146 | 147 | return parsedDetailScores 148 | } 149 | 150 | /** 151 | * Removes duplicate spam scores from the parsed details 152 | * @param {array} scores 153 | * @returns {array} 154 | */ 155 | async function deduplicateValues(scores) { 156 | // Some spam filters (like spamassassin) do add two distinct headers 157 | // with similar information to an email: 158 | // X-Spam-Status and X-Spam-Report where the first only contains a 159 | // list of checks that match an email whereas the latter contains 160 | // the corresponding scores and description as well. This leads to 161 | // double reporting of spam scores with different scores because 162 | // the parsing routine uses 0 to provide scores for tests where it 163 | // cannot find them. 164 | // See also https://github.com/friedPotat0/Spam-Scores/issues/48 165 | // To get rid of these duplicates, we use a two step approach: 166 | // 1. For all checks with a score of 0 the array is being examined 167 | // for another check of the same name that has a value being different 168 | // to 0 that can be used to update the score. (We do also add 169 | // descriptions provided by spamassassin in that step) 170 | // 2. Remove all duplicate checks from the array. 171 | // Unfortunately both passes do have a runtime of n^2. 172 | 173 | // 1. update all scores 174 | for (const el in scores) { 175 | for (const el2 in scores) { 176 | if (scores[el].name === scores[el2].name && scores[el2].score === 0) { 177 | if (scores[el].score > scores[el2].score || scores[el].score < scores[el2].score) { 178 | scores[el2].score = scores[el].score 179 | } else { 180 | scores[el].score = scores[el2].score 181 | } 182 | // while we're at it, update the description as well in 183 | // case it is missing 184 | if (!scores[el].description) { 185 | scores[el].description = scores[el2].description 186 | } else { 187 | scores[el2].description = scores[el].description 188 | } 189 | } 190 | } 191 | } 192 | // 2. remove duplicate checks -- https://stackoverflow.com/a/36744732 193 | const deduplicatedScores = scores.filter((el, index, self) => self.findIndex(el2 => el.name === el2.name) === index) 194 | return deduplicatedScores 195 | } 196 | 197 | /** 198 | * Trims and removes double whitespaces 199 | * @param {string} result 200 | * @returns {string} 201 | */ 202 | function sanitizeRegexResult(result) { 203 | return result?.trim()?.replace(/\s\s+/g, ' ') 204 | } 205 | -------------------------------------------------------------------------------- /src/typedef/general.js: -------------------------------------------------------------------------------- 1 | // For some reason it works for VSCode even if is not imported. 2 | 3 | // Constants 4 | 5 | /** 6 | * Defined Bounds for different scores 7 | * @typedef {object} interpolationBounds 8 | * @property {number} MIN_VALUE 9 | * @property {number} MAX_VALUE 10 | * @property {number} LOWER_BOUNDS 11 | * @property {number} UPPER_BOUNDS 12 | */ 13 | 14 | // Program 15 | 16 | /** 17 | * Messenger namespace. 18 | * In Thunderbird, all WebExtension API can be accessed through the messenger.* namespace, 19 | * as with Firefox, but also through the messenger.* namespace, which is a better fit for Thunderbird. 20 | * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API 21 | * https://webextension-api.thunderbird.net/en/91/#thunderbird-webextension-api-documentation 22 | * @namespace 23 | */ 24 | const messenger = { 25 | storage: { 26 | /** 27 | * @type {StorageArea} 28 | */ 29 | local: {}, 30 | /** 31 | * @type {StorageArea} 32 | */ 33 | sync: {}, 34 | /** 35 | * @type {StorageArea} 36 | */ 37 | managed: {} 38 | }, 39 | messages: { 40 | /** 41 | * Returns a specified message, including all headers and MIME parts. 42 | * 43 | * Required permissions [messagesRead] 44 | * @param {number} messageId 45 | * @returns {MessagePart} 46 | */ 47 | getFull: messageId => {} 48 | }, 49 | /** 50 | * messagesRead is required to use messageDisplay. 51 | */ 52 | messageDisplay: { 53 | /** 54 | * Fired when a message is displayed, whether in a 3-pane tab, a message tab, or a message window. 55 | * 56 | * Required permissions [messagesRead] 57 | */ 58 | onMessageDisplayed: { 59 | /** 60 | * Called when message is displayed 61 | * @param {function(Tab, MessageHeader)} listener 62 | */ 63 | addListener: listener => {} 64 | }, 65 | /** 66 | * Gets the currently displayed message in the specified tab. It returns null if no messages are displayed, or if multiple messages are displayed. 67 | * 68 | * Required permissions [messagesRead] 69 | * @param {number} tabId 70 | * @returns {Promise} 71 | */ 72 | getDisplayedMessage: tabId => {} 73 | }, 74 | /** 75 | * A manifest entry named message_display_action is required to use messageDisplayAction. 76 | */ 77 | messageDisplayAction: { 78 | /** 79 | * Disables the messageDisplayAction for a tab. 80 | * @param {number} [tabId] The id of the tab for which you want to modify the messageDisplayAction. 81 | */ 82 | disable: tabId => {}, 83 | /** 84 | * Enables the messageDisplayAction for a tab. By default, a messageDisplayAction is enabled. 85 | * @param {number} [tabId] The id of the tab for which you want to modify the messageDisplayAction. 86 | */ 87 | enable: tabId => {}, 88 | /** 89 | * Sets the title of the messageDisplayAction. This shows up in the tooltip and the label. Defaults to the add-on name. 90 | * @param {object} details 91 | * @param {string|null} details.title The string the messageDisplayAction should display as its label and when moused over. 92 | */ 93 | setTitle: details => {}, 94 | /** 95 | * Sets the icon for the messageDisplayAction. 96 | * The icon can be specified either as the path to an image file or as the pixel data from a canvas element, 97 | * or as dictionary of either one of those. Either the path or the imageData property must be specified. 98 | * @param {object} details 99 | * @param {ImageDataType | ImageDataDictionary} details.imageData (ImageDataType or ImageDataDictionary) - Either an ImageDataType object defining a single icon used for all sizes or an ImageDataDictionary object defining dedicated icons for different sizes. 100 | * @param {string | IconPathDictionary} details.path Either a relative image path defining a single icon used for all sizes or an IconPathDictionary object defining dedicated icons for different sizes. 101 | */ 102 | setIcon: details => {} 103 | }, 104 | tabs: { 105 | /** 106 | * Gets all tabs that have the specified properties, or all tabs if no properties are specified. 107 | * @param {object} [queryInfo] 108 | * @param {boolean} [queryInfo.active] Whether the tabs are active in their windows. 109 | * @param {boolean} [queryInfo.currentWindow] Whether the tabs are in the current window. 110 | * @param {boolean} [queryInfo.highlighted] Whether the tabs are highlighted. Works as an alias of active. 111 | * @param {number} [queryInfo.index] The position of the tabs within their windows. 112 | * @param {boolean} [queryInfo.lastFocusedWindow] Whether the tabs are in the last focused window. 113 | * @param {boolean} [queryInfo.mailTab] Whether the tab is a Thunderbird 3-pane tab. 114 | * @param {TabStatus} [queryInfo.status] Whether the tabs have completed loading. 115 | * @param {string} [queryInfo.title] Match page titles against a pattern. 116 | * @param {string} [queryInfo.type] – [Added in TB 91] Match tabs against the given Tab.type (see Tab). Ignored if queryInfo.mailTab is specified. 117 | * @param {string|string[]} [queryInfo.url] Match tabs against one or more URL Patterns. Note that fragment identifiers are not matched. 118 | * @param {number} [queryInfo.windowId] The ID of the parent window, or WINDOW_ID_CURRENT for the current window. 119 | * @param {WindowType} [queryInfo.windowType] The type of window the tabs are in. 120 | * @return {Promise} tabs 121 | */ 122 | query: queryInfo => {} 123 | }, 124 | i18n: { 125 | /** 126 | * Gets the localized string for the specified message. 127 | * @param {string} messageName 128 | * @param {string|string[]} [substitutions] 129 | * @returns {string} Message localized for current locale. 130 | */ 131 | getMessage: (messageName, substitutions) => '' 132 | }, 133 | /** 134 | * This module provides information about your extension and the environment it's running in. 135 | */ 136 | runtime: { 137 | /** 138 | * Retrieves the Window object for the background page running inside the current extension. 139 | * @returns {Promise} A Promise that will be fulfilled with the Window object for the background page, if there is one. 140 | * If the extension does not include a background page, the promise is rejected with an error message. 141 | */ 142 | getBackgroundPage: () => {} 143 | }, 144 | /** 145 | * @type {SpamScores} 146 | */ 147 | SpamScores: {} 148 | } 149 | 150 | /** 151 | * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/local 152 | * TODO: Incomplete 153 | * @typedef {object} StorageArea 154 | * @property {function(string[]=):Promise<(Object)>} get 155 | * @property {function(Object):Promise} set 156 | */ 157 | 158 | /** 159 | * https://webextension-api.thunderbird.net/en/91/messages.html#messages-messagepart 160 | * @typedef {object} MessagePart 161 | * @property {string} body The content of the part 162 | * @property {string} contentType 163 | * @property {Object} IconPathDictionary 177 | */ 178 | 179 | /** 180 | * Pixel data for an image. Must be an ImageData object (for example, from a canvas element). 181 | * @typedef {ImageData} ImageDataType 182 | */ 183 | 184 | /** 185 | * A {size: ImageDataType} dictionary representing the icon to be set. 186 | * The actual ImageDataType to be used is chosen depending on the screen’s pixel density. 187 | * See the MDN documentation on browser styles for more information on this. 188 | * At least one ImageDataType must be specified. 189 | * https://webextension-api.thunderbird.net/en/91/messageDisplayAction.html#messagedisplayaction-imagedatadictionary 190 | * @typedef {Object.} ImageDataDictionary 191 | */ 192 | 193 | /** 194 | * The type of a window. Under some circumstances a Window may not be assigned a type property. 195 | * @typedef {('normal'|'popup'|'panel'|'app'|'devtools'|'addressBook'|'messageCompose'|'messageDisplay')} WindowType 196 | */ 197 | 198 | /** 199 | * Whether the tabs have completed loading. 200 | * @typedef {('loading'|'complete')} TabStatus 201 | */ 202 | 203 | /** 204 | * https://webextension-api.thunderbird.net/en/91/tabs.html#tabs-tab 205 | * TODO: Incomplete 206 | * @typedef {object} Tab 207 | * @property {number} id The ID of the tab. Tab IDs are unique within a browser session. 208 | * Under some circumstances a Tab may not be assigned an ID. 209 | * Tab ID can also be set to TAB_ID_NONE for apps and devtools windows. 210 | */ 211 | 212 | /** 213 | * https://webextension-api.thunderbird.net/en/91/messages.html#messages-messageheader 214 | * TODO: Incomplete 215 | * @typedef {object} MessageHeader 216 | * @property {number} id 217 | */ 218 | 219 | /** 220 | * Custom 221 | * @typedef {object} parsedDetailScores 222 | * @property {string} name 223 | * @property {number} score 224 | * @property {string} info 225 | * @property {string} [description] 226 | */ 227 | 228 | /** 229 | * An object describing a mail folder. 230 | * https://webextension-api.thunderbird.net/en/91/folders.html#folders-mailfolder 231 | * TODO: Incomplete 232 | * @typedef {object} MailFolder 233 | * @property {string} path Path to this folder in the account. Although paths look predictable, never guess a folder’s path, as there are a number of reasons why it may not be what you think it is. 234 | */ 235 | -------------------------------------------------------------------------------- /src/options/options.js: -------------------------------------------------------------------------------- 1 | import { 2 | DEFAULT_SCORE_LOWER_BOUNDS, 3 | DEFAULT_SCORE_UPPER_BOUNDS, 4 | DEFAULT_SCORE_HEADER_ORDER, 5 | DEFAULT_SCORE_DETAILS_ORDER 6 | } from '../constants.js' 7 | import { getBounds } from '../functions.js' 8 | 9 | // Variables 10 | const localStorage = messenger.storage.local 11 | const i18n = messenger.i18n 12 | 13 | // DOM variables 14 | const inputScoreBoundsLower = document.getElementById('score-bounds-lower') 15 | const inputScoreBoundsUpper = document.getElementById('score-bounds-upper') 16 | const inputScoreBoundsBetween = document.getElementById('score-bounds-between') 17 | const checkboxIconScorePositive = document.getElementById('hide-icon-score-positive') 18 | const checkboxIconScoreNeutral = document.getElementById('hide-icon-score-neutral') 19 | const checkboxIconScoreNegative = document.getElementById('hide-icon-score-negative') 20 | const scoreHeadersList = document.getElementById('score-headers-list') 21 | const scoreDetailsHeadersList = document.getElementById('score-details-headers-list') 22 | const resetScoreHeadersBtn = document.getElementById('reset-score-headers') 23 | const resetScoreDetailsHeadersBtn = document.getElementById('reset-score-details-headers') 24 | 25 | // Translations 26 | for (const i18nKey of [ 27 | 'optionsIconRanges', 28 | 'optionsScoreGreater', 29 | 'optionsScoreLess', 30 | 'optionsHeaderPriority', 31 | 'optionsHeaderPriorityDescription', 32 | 'optionsScoreHeaders', 33 | 'optionsScoreDetailsHeaders' 34 | ]) { 35 | const element = document.querySelector('*[data-i18n="' + i18nKey + '"]') 36 | if (element) element.textContent = i18n.getMessage(i18nKey) 37 | } 38 | document 39 | .querySelectorAll('*[data-i18n="optionsHideIconAndScore"]') 40 | .forEach(el => (el.textContent = i18n.getMessage('optionsHideIconAndScore'))) 41 | document 42 | .querySelectorAll('*[data-i18n="optionsResetToDefault"]') 43 | .forEach(el => (el.textContent = i18n.getMessage('optionsResetToDefault'))) 44 | 45 | // DOM events 46 | inputScoreBoundsLower.addEventListener('change', saveScoreLower) 47 | inputScoreBoundsUpper.addEventListener('change', saveScoreUpper) 48 | checkboxIconScorePositive.addEventListener('change', saveIconPositive) 49 | checkboxIconScoreNeutral.addEventListener('change', saveIconNeutral) 50 | checkboxIconScoreNegative.addEventListener('change', saveIconNegative) 51 | resetScoreHeadersBtn.addEventListener('click', resetScoreHeadersOrder) 52 | resetScoreDetailsHeadersBtn.addEventListener('click', resetScoreDetailsHeadersOrder) 53 | 54 | async function init() { 55 | // Load values from storage 56 | const storage = await localStorage.get([ 57 | 'scoreIconLowerBounds', 58 | 'scoreIconUpperBounds', 59 | 'hideIconScorePositive', 60 | 'hideIconScoreNeutral', 61 | 'hideIconScoreNegative', 62 | 'scoreHeaderOrder', 63 | 'scoreDetailsHeaderOrder' 64 | ]) 65 | 66 | const [lowerBounds, upperBounds] = getBounds(storage) 67 | 68 | // Set values 69 | inputScoreBoundsLower.value = lowerBounds 70 | inputScoreBoundsUpper.value = upperBounds 71 | inputScoreBoundsBetween.textContent = i18n.getMessage('optionsScoreBetween', [lowerBounds, upperBounds]) 72 | messenger.SpamScores.setScoreBounds(lowerBounds, upperBounds) 73 | 74 | checkboxIconScorePositive.checked = storage.hideIconScorePositive || false 75 | checkboxIconScoreNeutral.checked = storage.hideIconScoreNeutral || false 76 | checkboxIconScoreNegative.checked = storage.hideIconScoreNegative || false 77 | 78 | // Initialize header order lists 79 | const scoreHeaderOrder = storage.scoreHeaderOrder || DEFAULT_SCORE_HEADER_ORDER 80 | const scoreDetailsHeaderOrder = storage.scoreDetailsHeaderOrder || DEFAULT_SCORE_DETAILS_ORDER 81 | 82 | renderSortableList(scoreHeadersList, scoreHeaderOrder, 'scoreHeaderOrder') 83 | renderSortableList(scoreDetailsHeadersList, scoreDetailsHeaderOrder, 'scoreDetailsHeaderOrder') 84 | } 85 | init() 86 | 87 | function saveIconPositive() { 88 | const hideIconScorePositive = checkboxIconScorePositive.checked 89 | localStorage.set({ hideIconScorePositive }) 90 | saveIcons() 91 | } 92 | 93 | function saveIconNeutral() { 94 | const hideIconScoreNeutral = checkboxIconScoreNeutral.checked 95 | localStorage.set({ hideIconScoreNeutral }) 96 | saveIcons() 97 | } 98 | 99 | function saveIconNegative() { 100 | const hideIconScoreNegative = checkboxIconScoreNegative.checked 101 | localStorage.set({ hideIconScoreNegative }) 102 | saveIcons() 103 | } 104 | 105 | function saveIcons() { 106 | // Get rest of values 107 | const hideIconScorePositive = checkboxIconScorePositive.checked 108 | const hideIconScoreNeutral = checkboxIconScoreNeutral.checked 109 | const hideIconScoreNegative = checkboxIconScoreNegative.checked 110 | 111 | messenger.SpamScores.setHideIconScoreOptions(hideIconScorePositive, hideIconScoreNeutral, hideIconScoreNegative) 112 | } 113 | 114 | async function saveScoreLower() { 115 | const scoreBoundsLower = inputScoreBoundsLower.value 116 | const storage = await localStorage.get(['scoreIconLowerBounds', 'scoreIconUpperBounds']) 117 | const newUpperBounds = parseFloat(storage.scoreIconUpperBounds || DEFAULT_SCORE_UPPER_BOUNDS) 118 | 119 | if (scoreBoundsLower !== '') { 120 | let newLowerBounds = parseFloat(scoreBoundsLower) 121 | try { 122 | if (newLowerBounds > newUpperBounds) throw Error('Upper score cannot be lower than lower bounds') 123 | if (newLowerBounds < -10000) throw Error('Wrong score lower bounds') 124 | localStorage.set({ scoreIconLowerBounds: newLowerBounds }) 125 | saveScores(newLowerBounds, newUpperBounds) 126 | } catch (error) { 127 | // Restore previously saved bounds or fallback to defaults 128 | newLowerBounds = parseFloat(storage.scoreIconLowerBounds || DEFAULT_SCORE_LOWER_BOUNDS) 129 | } 130 | inputScoreBoundsLower.value = newLowerBounds // number 131 | } 132 | } 133 | 134 | async function saveScoreUpper() { 135 | const scoreBoundsUpper = inputScoreBoundsUpper.value 136 | const storage = await localStorage.get(['scoreIconLowerBounds', 'scoreIconUpperBounds']) 137 | const newLowerBounds = parseFloat(storage.scoreIconLowerBounds || DEFAULT_SCORE_LOWER_BOUNDS) 138 | 139 | if (scoreBoundsUpper !== '') { 140 | let newUpperBounds = parseFloat(scoreBoundsUpper) 141 | try { 142 | if (newLowerBounds > newUpperBounds) throw Error('Upper score cannot be lower than lower bounds') 143 | if (newUpperBounds > 10000) throw Error('Wrong score upper bounds') 144 | localStorage.set({ scoreIconUpperBounds: newUpperBounds }) 145 | saveScores(newLowerBounds, newUpperBounds) 146 | } catch (error) { 147 | // Restore previously saved bounds or fallback to defaults 148 | newUpperBounds = parseFloat(storage.scoreIconUpperBounds || DEFAULT_SCORE_UPPER_BOUNDS) 149 | } 150 | inputScoreBoundsUpper.value = newUpperBounds // number 151 | } 152 | } 153 | 154 | function saveScores(lower, upper) { 155 | inputScoreBoundsBetween.textContent = i18n.getMessage('optionsScoreBetween', [lower, upper]) 156 | messenger.SpamScores.setScoreBounds(lower, upper) 157 | } 158 | 159 | /** 160 | * Render a sortable list of headers 161 | * @param {HTMLElement} container - The container element for the list 162 | * @param {string[]} headers - Array of header names 163 | * @param {string} storageKey - Key for localStorage 164 | */ 165 | function renderSortableList(container, headers, storageKey) { 166 | container.innerHTML = '' 167 | 168 | headers.forEach((header, index) => { 169 | const item = document.createElement('div') 170 | item.className = 'sortable-item' 171 | item.textContent = header 172 | item.draggable = true 173 | item.dataset.index = index 174 | 175 | item.addEventListener('dragstart', handleDragStart) 176 | item.addEventListener('dragover', handleDragOver) 177 | item.addEventListener('drop', e => handleDrop(e, container, storageKey)) 178 | item.addEventListener('dragend', handleDragEnd) 179 | 180 | container.appendChild(item) 181 | }) 182 | } 183 | 184 | let draggedElement = null 185 | 186 | function handleDragStart(e) { 187 | draggedElement = e.target 188 | e.target.classList.add('dragging') 189 | e.dataTransfer.effectAllowed = 'move' 190 | } 191 | 192 | function handleDragOver(e) { 193 | if (e.preventDefault) { 194 | e.preventDefault() 195 | } 196 | e.dataTransfer.dropEffect = 'move' 197 | return false 198 | } 199 | 200 | function handleDrop(e, container, storageKey) { 201 | if (e.stopPropagation) { 202 | e.stopPropagation() 203 | } 204 | 205 | if (draggedElement !== e.target && e.target.classList.contains('sortable-item')) { 206 | // Get all items 207 | const items = Array.from(container.querySelectorAll('.sortable-item')) 208 | const draggedIndex = items.indexOf(draggedElement) 209 | const targetIndex = items.indexOf(e.target) 210 | 211 | // Reorder in DOM 212 | if (draggedIndex < targetIndex) { 213 | e.target.parentNode.insertBefore(draggedElement, e.target.nextSibling) 214 | } else { 215 | e.target.parentNode.insertBefore(draggedElement, e.target) 216 | } 217 | 218 | // Save new order 219 | saveHeaderOrder(container, storageKey) 220 | } 221 | 222 | return false 223 | } 224 | 225 | function handleDragEnd(e) { 226 | e.target.classList.remove('dragging') 227 | } 228 | 229 | /** 230 | * Save the current order of headers to localStorage 231 | * @param {HTMLElement} container - The container element 232 | * @param {string} storageKey - Key for localStorage 233 | */ 234 | async function saveHeaderOrder(container, storageKey) { 235 | const items = Array.from(container.querySelectorAll('.sortable-item')) 236 | const order = items.map(item => item.textContent) 237 | 238 | await localStorage.set({ [storageKey]: order }) 239 | 240 | // Update experiments.js 241 | if (storageKey === 'scoreHeaderOrder') { 242 | messenger.SpamScores.setScoreHeaderOrder(order) 243 | } else if (storageKey === 'scoreDetailsHeaderOrder') { 244 | messenger.SpamScores.setScoreDetailsHeaderOrder(order) 245 | } 246 | } 247 | 248 | /** 249 | * Reset score headers to default order 250 | */ 251 | async function resetScoreHeadersOrder() { 252 | await localStorage.set({ scoreHeaderOrder: DEFAULT_SCORE_HEADER_ORDER }) 253 | renderSortableList(scoreHeadersList, DEFAULT_SCORE_HEADER_ORDER, 'scoreHeaderOrder') 254 | messenger.SpamScores.setScoreHeaderOrder(DEFAULT_SCORE_HEADER_ORDER) 255 | } 256 | 257 | /** 258 | * Reset score details headers to default order 259 | */ 260 | async function resetScoreDetailsHeadersOrder() { 261 | await localStorage.set({ scoreDetailsHeaderOrder: DEFAULT_SCORE_DETAILS_ORDER }) 262 | renderSortableList(scoreDetailsHeadersList, DEFAULT_SCORE_DETAILS_ORDER, 'scoreDetailsHeaderOrder') 263 | messenger.SpamScores.setScoreDetailsHeaderOrder(DEFAULT_SCORE_DETAILS_ORDER) 264 | } 265 | -------------------------------------------------------------------------------- /src/experiments/experiments.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Copy of constants.js until Experiments API supports ES6 modules 4 | const CUSTOM_SCORE_REGEX = { 5 | 'mailscanner-spamcheck': 6 | /(?:score|punteggio|puntuació|sgor\/score|skore|Wertung|bedømmelse|puntaje|pont|escore|resultat|skore)=([-+]?[0-9]+\.?[0-9]*),/ 7 | } 8 | 9 | // Copy of constants.js until Experiments API supports ES6 modules 10 | const SCORE_REGEX = { 11 | 'x-spamd-result': /\[([-+]?[0-9]+\.?[0-9]*) \/ [-+]?[0-9]+\.?[0-9]*\];/, 12 | 'x-spam-status': /(?:Yes|No)(?:, score=|\/)([-+]?[0-9]+\.?[0-9]*)/, 13 | 'x-rspam-status': /(?:Yes|No)(?:, score=|\/)([-+]?[0-9]+\.?[0-9]*)/, 14 | 'x-spam-score': /([-+]?[0-9]+\.?[0-9]*)/, 15 | 'x-spam-report': /([-+]?[0-9]+\.?[0-9]*) hits,/, 16 | 'x-ham-report': /([-+]?[0-9]+\.?[0-9]*) hits,/, 17 | 'x-rspamd-score': /([-+]?[0-9]+\.?[0-9]*)/, 18 | 'x-vr-spamscore': /([0-9]+)/, 19 | 'x-hmailserver-reason-score': /([-+]?[0-9]+\.?[0-9]*)/ 20 | } 21 | 22 | // Default order for parsing score headers 23 | const DEFAULT_SCORE_HEADER_ORDER = [ 24 | 'x-spamd-result', 25 | 'x-spam-status', 26 | 'x-rspam-status', 27 | 'x-spam-score', 28 | 'x-spam-report', 29 | 'x-ham-report', 30 | 'x-rspamd-score', 31 | 'x-vr-spamscore', 32 | 'x-hmailserver-reason-score' 33 | ] 34 | 35 | function importThreadPaneColumnsModule() { 36 | try { 37 | // TB115 38 | return ChromeUtils.importESModule('chrome://messenger/content/thread-pane-columns.mjs') 39 | } catch (err) { 40 | // TB128 41 | return ChromeUtils.importESModule('chrome://messenger/content/ThreadPaneColumns.mjs') 42 | } 43 | } 44 | 45 | var { ThreadPaneColumns } = importThreadPaneColumnsModule() 46 | 47 | const DEFAULT_SCORE_LOWER_BOUNDS = -2 48 | const DEFAULT_SCORE_UPPER_BOUNDS = 2 49 | 50 | let scoreHdrViewParams = { 51 | lowerScoreBounds: DEFAULT_SCORE_LOWER_BOUNDS, 52 | upperScoreBounds: DEFAULT_SCORE_UPPER_BOUNDS, 53 | scoreHeaderOrder: DEFAULT_SCORE_HEADER_ORDER 54 | } 55 | 56 | function getScore(hdr) { 57 | // Use custom order if available, otherwise use default 58 | const headerOrder = scoreHdrViewParams.scoreHeaderOrder || DEFAULT_SCORE_HEADER_ORDER 59 | 60 | for (const regExName of headerOrder) { 61 | if (!SCORE_REGEX[regExName]) continue // Skip if regex not defined 62 | const headerValue = hdr.getStringProperty(regExName) 63 | if (headerValue === '') continue 64 | const scoreField = headerValue.match(SCORE_REGEX[regExName]) 65 | if (!scoreField) continue // If no match iterate - Note: This shouldn't be needed 66 | const score = parseFloat(scoreField[1]) 67 | if (!isNaN(score)) return score 68 | } 69 | 70 | if (scoreHdrViewParams.customMailscannerHeaders) { 71 | for (const headerName of scoreHdrViewParams.customMailscannerHeaders) { 72 | for (const regExName in CUSTOM_SCORE_REGEX) { 73 | if (headerName.endsWith(regExName)) { 74 | const headerValue = hdr.getStringProperty(headerName) 75 | const scoreField = headerValue.match(CUSTOM_SCORE_REGEX[regExName]) 76 | if (!scoreField) continue // If no match iterate 77 | const score = parseFloat(scoreField[1]) 78 | if (!isNaN(score)) return score 79 | } 80 | } 81 | } 82 | } 83 | return null 84 | } 85 | 86 | function getSortScore(hdr) { 87 | const score = getScore(hdr) 88 | if (score === null) return null 89 | // Multiply by 100000 for decimal precision, then add offset of 1 billion to handle negative numbers 90 | // This ensures both negative and positive scores sort correctly 91 | return Math.round(score * 100000) + 1000000000 92 | } 93 | 94 | /** 95 | * For debugging Experiments.js, press CTRL + SHIFT + U/I 96 | */ 97 | 98 | /** 99 | * Do not change var because it's a global class 100 | * https://webextension-api.thunderbird.net/en/91/how-to/experiments.html#implementing-functions 101 | */ 102 | var SpamScores = class extends ExtensionAPI { 103 | /** 104 | * Called on startup and on reload 105 | */ 106 | onStartup() { 107 | updatePrefs() 108 | } 109 | 110 | /** 111 | * Called when the extension is disabled, removed, reloaded, or Thunderbird closes. 112 | * @param {boolean} isAppShutdown 113 | */ 114 | onShutdown(isAppShutdown) { 115 | if (isAppShutdown) return 116 | /** 117 | * This method is called to notify all observers for a particular topic. See Example. 118 | * .notifyObservers(null, "myTopicID", "someAdditionalInformationPassedAs'Data'Parameter"); 119 | * 120 | * void notifyObservers(in nsISupports aSubject, in string aTopic, in wstring someData); 121 | * aSubject A notification specific interface pointer. This usually corresponds to the source of the notification, but could be defined differently depending on the notification topic and may even be null. 122 | * aTopic The notification topic. This string-valued key uniquely identifies the notification. This parameter must not be null. 123 | * someData A notification specific string value. The meaning of this parameter is dependent on the topic. It may be null. 124 | */ 125 | Services.obs.notifyObservers(null, 'startupcache-invalidate') 126 | } 127 | 128 | /** 129 | * 130 | * @param {*} context 131 | * @returns 132 | */ 133 | getAPI(context) { 134 | context.callOnClose(this) 135 | // All functions should be added in schema.json 136 | return { 137 | SpamScores: { 138 | setScoreBounds(lower, upper) { 139 | scoreHdrViewParams.lowerScoreBounds = lower 140 | scoreHdrViewParams.upperScoreBounds = upper 141 | }, 142 | setHideIconScoreOptions(hidePositive, hideNeutral, hideNegative) { 143 | scoreHdrViewParams.hideIconScorePositive = hidePositive 144 | scoreHdrViewParams.hideIconScoreNeutral = hideNeutral 145 | scoreHdrViewParams.hideIconScoreNegative = hideNegative 146 | }, 147 | setCustomMailscannerHeaders(customMailscannerHeaders) { 148 | scoreHdrViewParams.customMailscannerHeaders = customMailscannerHeaders 149 | }, 150 | setScoreHeaderOrder(scoreHeaderOrder) { 151 | scoreHdrViewParams.scoreHeaderOrder = scoreHeaderOrder 152 | }, 153 | setScoreDetailsHeaderOrder(scoreDetailsHeaderOrder) { 154 | scoreHdrViewParams.scoreDetailsHeaderOrder = scoreDetailsHeaderOrder 155 | }, 156 | addHeadersToPrefs(dynamicHeaders) { 157 | updatePrefs(dynamicHeaders) 158 | }, 159 | // Deprecated (Fallback for add-on version <= 1.3.1) 160 | getHelloFlag() { 161 | try { 162 | return Services.prefs.getBoolPref('spamscores.hello') 163 | } catch (err) { 164 | return false 165 | } 166 | }, 167 | 168 | async addColumns(nameSpamScoreValue, nameSpamScoreIcon) { 169 | function getExtensionUrl(url) { 170 | if (url) { 171 | return context.extension.baseURI.resolve(url) 172 | } 173 | return null 174 | } 175 | function scoreCallback(msgHdr) { 176 | let score = getScore(msgHdr) 177 | if (score === null) return null 178 | if (score > scoreHdrViewParams.upperScoreBounds && scoreHdrViewParams.hideIconScorePositive) return null 179 | if ( 180 | score <= scoreHdrViewParams.upperScoreBounds && 181 | score >= scoreHdrViewParams.lowerScoreBounds && 182 | scoreHdrViewParams.hideIconScoreNeutral 183 | ) 184 | return null 185 | if (score < scoreHdrViewParams.lowerScoreBounds && scoreHdrViewParams.hideIconScoreNegative) return null 186 | return score 187 | } 188 | 189 | ThreadPaneColumns.addCustomColumn('spam-score-value', { 190 | name: nameSpamScoreValue, 191 | hidden: true, 192 | icon: false, 193 | resizable: true, 194 | sortable: true, 195 | sortCallback: getSortScore, 196 | textCallback: scoreCallback 197 | }) 198 | 199 | ThreadPaneColumns.addCustomColumn('spam-score-icon', { 200 | name: nameSpamScoreIcon, 201 | hidden: true, 202 | icon: true, 203 | iconHeaderUrl: getExtensionUrl('/images/icon-16px.png'), 204 | iconCellDefinitions: [ 205 | { 206 | id: 'positive', 207 | alt: '+', 208 | title: 'Positive Spam Score', 209 | url: getExtensionUrl('/images/score_positive.png') 210 | }, 211 | { 212 | id: 'negative', 213 | alt: '-', 214 | title: 'Negative Spam Score', 215 | url: getExtensionUrl('/images/score_negative.png') 216 | }, 217 | { 218 | id: 'neutral', 219 | alt: '0', 220 | title: 'Neutral Spam Score', 221 | url: getExtensionUrl('/images/score_neutral.png') 222 | } 223 | ], 224 | iconCallback: msgHdr => { 225 | let score = getScore(msgHdr) 226 | if (score === null) return '' 227 | if (!scoreHdrViewParams.hideIconScorePositive && score > scoreHdrViewParams.upperScoreBounds) 228 | return 'positive' 229 | if ( 230 | !scoreHdrViewParams.hideIconScoreNeutral && 231 | score <= scoreHdrViewParams.upperScoreBounds && 232 | score >= scoreHdrViewParams.lowerScoreBounds 233 | ) 234 | return 'neutral' 235 | if (!scoreHdrViewParams.hideIconScoreNegative && score < scoreHdrViewParams.lowerScoreBounds) 236 | return 'negative' 237 | return '' 238 | }, 239 | resizable: false, 240 | sortable: true, 241 | sortCallback: getSortScore, 242 | textCallback: scoreCallback 243 | }) 244 | }, 245 | 246 | async removeColumns(id) { 247 | ThreadPaneColumns.removeCustomColumn('spam-score-value') 248 | ThreadPaneColumns.removeCustomColumn('spam-score-icon') 249 | } 250 | } 251 | } 252 | } 253 | 254 | close() { 255 | ThreadPaneColumns.removeCustomColumn('spam-score-value') 256 | ThreadPaneColumns.removeCustomColumn('spam-score-icon') 257 | } 258 | } 259 | 260 | /** 261 | * This is what it lets nsIMsgDBHdr have the properties of the headers 262 | * http://kb.mozillazine.org/Mail_and_news_settings // 2019 263 | * http://udn.realityripple.com/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPrefBranch 264 | * https://searchfox.org/comm-central/source/mailnews/mailnews.js // 2021 265 | * Requirements: Repair Folders then Restart 266 | * @param {string[]} dynamicHeaders 267 | */ 268 | function updatePrefs(dynamicHeaders = []) { 269 | const mailnews = Services.prefs.getBranch('mailnews') 270 | // Copy of constants.js until support of ES6 modules 271 | const staticHeaders = [ 272 | 'x-spam-score', 273 | 'x-rspamd-score', 274 | 'x-vr-spamscore', 275 | 'x-spamd-result', 276 | 'x-spam-status', 277 | 'x-rspam-status', 278 | 'x-spam-report', 279 | 'x-ham-report', 280 | 'x-hmailserver-reason-score' 281 | ] 282 | const headers = [...staticHeaders, ...dynamicHeaders] 283 | 284 | // customDBHeaders: String in the form of "header1 header2 header3" 285 | // Note: Do not overwrite headers of other add-ons or user-defined ones. Always append new headers! 286 | const existingCustomDBHeaders = Services.prefs.getCharPref('mailnews.customDBHeaders').trim() 287 | // Split existing headers and filter out new ones that already exist 288 | const existingDBHeadersArray = existingCustomDBHeaders ? existingCustomDBHeaders.split(/\s+/) : [] 289 | let newCustomDBHeaders = headers.filter(el => !existingDBHeadersArray.includes(el)) 290 | if (newCustomDBHeaders.length > 0) { 291 | const separator = existingCustomDBHeaders ? ' ' : '' 292 | newCustomDBHeaders = `${existingCustomDBHeaders}${separator}${newCustomDBHeaders.join(' ')}` 293 | mailnews.setCharPref('.customDBHeaders', newCustomDBHeaders) 294 | } 295 | 296 | // customHeaders: String in the form of "header1: header2: header3:" 297 | // Note: Do not overwrite headers of other add-ons or user-defined ones. Always append new headers! 298 | const existingCustomHeaders = Services.prefs.getCharPref('mailnews.customHeaders').trim() 299 | // Split existing headers by ": " and filter out new ones that already exist 300 | const existingHeadersArray = existingCustomHeaders 301 | ? existingCustomHeaders 302 | .split(/:\s*/) 303 | .filter(h => h) 304 | .map(h => h.trim()) 305 | : [] 306 | let newCustomHeaders = headers.filter(el => !existingHeadersArray.includes(el)) 307 | if (newCustomHeaders.length > 0) { 308 | // Ensure proper format: existing headers should end with ":" and new headers should be separated by ": " 309 | // If existing headers exist and don't end with ":", add ": " before new headers 310 | // If existing headers end with ":", add " " before new headers 311 | let prefix = existingCustomHeaders 312 | if (existingCustomHeaders) { 313 | if (existingCustomHeaders.endsWith(':')) { 314 | prefix = existingCustomHeaders + ' ' 315 | } else if (existingCustomHeaders.endsWith(': ')) { 316 | prefix = existingCustomHeaders 317 | } else { 318 | prefix = existingCustomHeaders + ': ' 319 | } 320 | } 321 | newCustomHeaders = `${prefix}${newCustomHeaders.join(': ')}:` 322 | mailnews.setCharPref('.customHeaders', newCustomHeaders) 323 | } 324 | 325 | /** 326 | * PREF_INVALID 0 long 327 | * PREF_STRING 32 long data type. 328 | * PREF_INT 64 long data type. 329 | * PREF_BOOL 128 long data type. 330 | */ 331 | // console.log(mailnews.getPrefType('.customDBHeaders')) 332 | } 333 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Attribution-NonCommercial-ShareAlike 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 58 | Public License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-NonCommercial-ShareAlike 4.0 International Public License 63 | ("Public License"). To the extent this Public License may be 64 | interpreted as a contract, You are granted the Licensed Rights in 65 | consideration of Your acceptance of these terms and conditions, and the 66 | Licensor grants You such rights in consideration of benefits the 67 | Licensor receives from making the Licensed Material available under 68 | these terms and conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Adapter's License means the license You apply to Your Copyright 84 | and Similar Rights in Your contributions to Adapted Material in 85 | accordance with the terms and conditions of this Public License. 86 | 87 | c. BY-NC-SA Compatible License means a license listed at 88 | creativecommons.org/compatiblelicenses, approved by Creative 89 | Commons as essentially the equivalent of this Public License. 90 | 91 | d. Copyright and Similar Rights means copyright and/or similar rights 92 | closely related to copyright including, without limitation, 93 | performance, broadcast, sound recording, and Sui Generis Database 94 | Rights, without regard to how the rights are labeled or 95 | categorized. For purposes of this Public License, the rights 96 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 97 | Rights. 98 | 99 | e. Effective Technological Measures means those measures that, in the 100 | absence of proper authority, may not be circumvented under laws 101 | fulfilling obligations under Article 11 of the WIPO Copyright 102 | Treaty adopted on December 20, 1996, and/or similar international 103 | agreements. 104 | 105 | f. Exceptions and Limitations means fair use, fair dealing, and/or 106 | any other exception or limitation to Copyright and Similar Rights 107 | that applies to Your use of the Licensed Material. 108 | 109 | g. License Elements means the license attributes listed in the name 110 | of a Creative Commons Public License. The License Elements of this 111 | Public License are Attribution, NonCommercial, and ShareAlike. 112 | 113 | h. Licensed Material means the artistic or literary work, database, 114 | or other material to which the Licensor applied this Public 115 | License. 116 | 117 | i. Licensed Rights means the rights granted to You subject to the 118 | terms and conditions of this Public License, which are limited to 119 | all Copyright and Similar Rights that apply to Your use of the 120 | Licensed Material and that the Licensor has authority to license. 121 | 122 | j. Licensor means the individual(s) or entity(ies) granting rights 123 | under this Public License. 124 | 125 | k. NonCommercial means not primarily intended for or directed towards 126 | commercial advantage or monetary compensation. For purposes of 127 | this Public License, the exchange of the Licensed Material for 128 | other material subject to Copyright and Similar Rights by digital 129 | file-sharing or similar means is NonCommercial provided there is 130 | no payment of monetary compensation in connection with the 131 | exchange. 132 | 133 | l. Share means to provide material to the public by any means or 134 | process that requires permission under the Licensed Rights, such 135 | as reproduction, public display, public performance, distribution, 136 | dissemination, communication, or importation, and to make material 137 | available to the public including in ways that members of the 138 | public may access the material from a place and at a time 139 | individually chosen by them. 140 | 141 | m. Sui Generis Database Rights means rights other than copyright 142 | resulting from Directive 96/9/EC of the European Parliament and of 143 | the Council of 11 March 1996 on the legal protection of databases, 144 | as amended and/or succeeded, as well as other essentially 145 | equivalent rights anywhere in the world. 146 | 147 | n. You means the individual or entity exercising the Licensed Rights 148 | under this Public License. Your has a corresponding meaning. 149 | 150 | 151 | Section 2 -- Scope. 152 | 153 | a. License grant. 154 | 155 | 1. Subject to the terms and conditions of this Public License, 156 | the Licensor hereby grants You a worldwide, royalty-free, 157 | non-sublicensable, non-exclusive, irrevocable license to 158 | exercise the Licensed Rights in the Licensed Material to: 159 | 160 | a. reproduce and Share the Licensed Material, in whole or 161 | in part, for NonCommercial purposes only; and 162 | 163 | b. produce, reproduce, and Share Adapted Material for 164 | NonCommercial purposes only. 165 | 166 | 2. Exceptions and Limitations. For the avoidance of doubt, where 167 | Exceptions and Limitations apply to Your use, this Public 168 | License does not apply, and You do not need to comply with 169 | its terms and conditions. 170 | 171 | 3. Term. The term of this Public License is specified in Section 172 | 6(a). 173 | 174 | 4. Media and formats; technical modifications allowed. The 175 | Licensor authorizes You to exercise the Licensed Rights in 176 | all media and formats whether now known or hereafter created, 177 | and to make technical modifications necessary to do so. The 178 | Licensor waives and/or agrees not to assert any right or 179 | authority to forbid You from making technical modifications 180 | necessary to exercise the Licensed Rights, including 181 | technical modifications necessary to circumvent Effective 182 | Technological Measures. For purposes of this Public License, 183 | simply making modifications authorized by this Section 2(a) 184 | (4) never produces Adapted Material. 185 | 186 | 5. Downstream recipients. 187 | 188 | a. Offer from the Licensor -- Licensed Material. Every 189 | recipient of the Licensed Material automatically 190 | receives an offer from the Licensor to exercise the 191 | Licensed Rights under the terms and conditions of this 192 | Public License. 193 | 194 | b. Additional offer from the Licensor -- Adapted Material. 195 | Every recipient of Adapted Material from You 196 | automatically receives an offer from the Licensor to 197 | exercise the Licensed Rights in the Adapted Material 198 | under the conditions of the Adapter's License You apply. 199 | 200 | c. No downstream restrictions. You may not offer or impose 201 | any additional or different terms or conditions on, or 202 | apply any Effective Technological Measures to, the 203 | Licensed Material if doing so restricts exercise of the 204 | Licensed Rights by any recipient of the Licensed 205 | Material. 206 | 207 | 6. No endorsement. Nothing in this Public License constitutes or 208 | may be construed as permission to assert or imply that You 209 | are, or that Your use of the Licensed Material is, connected 210 | with, or sponsored, endorsed, or granted official status by, 211 | the Licensor or others designated to receive attribution as 212 | provided in Section 3(a)(1)(A)(i). 213 | 214 | b. Other rights. 215 | 216 | 1. Moral rights, such as the right of integrity, are not 217 | licensed under this Public License, nor are publicity, 218 | privacy, and/or other similar personality rights; however, to 219 | the extent possible, the Licensor waives and/or agrees not to 220 | assert any such rights held by the Licensor to the limited 221 | extent necessary to allow You to exercise the Licensed 222 | Rights, but not otherwise. 223 | 224 | 2. Patent and trademark rights are not licensed under this 225 | Public License. 226 | 227 | 3. To the extent possible, the Licensor waives any right to 228 | collect royalties from You for the exercise of the Licensed 229 | Rights, whether directly or through a collecting society 230 | under any voluntary or waivable statutory or compulsory 231 | licensing scheme. In all other cases the Licensor expressly 232 | reserves any right to collect such royalties, including when 233 | the Licensed Material is used other than for NonCommercial 234 | purposes. 235 | 236 | 237 | Section 3 -- License Conditions. 238 | 239 | Your exercise of the Licensed Rights is expressly made subject to the 240 | following conditions. 241 | 242 | a. Attribution. 243 | 244 | 1. If You Share the Licensed Material (including in modified 245 | form), You must: 246 | 247 | a. retain the following if it is supplied by the Licensor 248 | with the Licensed Material: 249 | 250 | i. identification of the creator(s) of the Licensed 251 | Material and any others designated to receive 252 | attribution, in any reasonable manner requested by 253 | the Licensor (including by pseudonym if 254 | designated); 255 | 256 | ii. a copyright notice; 257 | 258 | iii. a notice that refers to this Public License; 259 | 260 | iv. a notice that refers to the disclaimer of 261 | warranties; 262 | 263 | v. a URI or hyperlink to the Licensed Material to the 264 | extent reasonably practicable; 265 | 266 | b. indicate if You modified the Licensed Material and 267 | retain an indication of any previous modifications; and 268 | 269 | c. indicate the Licensed Material is licensed under this 270 | Public License, and include the text of, or the URI or 271 | hyperlink to, this Public License. 272 | 273 | 2. You may satisfy the conditions in Section 3(a)(1) in any 274 | reasonable manner based on the medium, means, and context in 275 | which You Share the Licensed Material. For example, it may be 276 | reasonable to satisfy the conditions by providing a URI or 277 | hyperlink to a resource that includes the required 278 | information. 279 | 3. If requested by the Licensor, You must remove any of the 280 | information required by Section 3(a)(1)(A) to the extent 281 | reasonably practicable. 282 | 283 | b. ShareAlike. 284 | 285 | In addition to the conditions in Section 3(a), if You Share 286 | Adapted Material You produce, the following conditions also apply. 287 | 288 | 1. The Adapter's License You apply must be a Creative Commons 289 | license with the same License Elements, this version or 290 | later, or a BY-NC-SA Compatible License. 291 | 292 | 2. You must include the text of, or the URI or hyperlink to, the 293 | Adapter's License You apply. You may satisfy this condition 294 | in any reasonable manner based on the medium, means, and 295 | context in which You Share Adapted Material. 296 | 297 | 3. You may not offer or impose any additional or different terms 298 | or conditions on, or apply any Effective Technological 299 | Measures to, Adapted Material that restrict exercise of the 300 | rights granted under the Adapter's License You apply. 301 | 302 | 303 | Section 4 -- Sui Generis Database Rights. 304 | 305 | Where the Licensed Rights include Sui Generis Database Rights that 306 | apply to Your use of the Licensed Material: 307 | 308 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 309 | to extract, reuse, reproduce, and Share all or a substantial 310 | portion of the contents of the database for NonCommercial purposes 311 | only; 312 | 313 | b. if You include all or a substantial portion of the database 314 | contents in a database in which You have Sui Generis Database 315 | Rights, then the database in which You have Sui Generis Database 316 | Rights (but not its individual contents) is Adapted Material, 317 | including for purposes of Section 3(b); and 318 | 319 | c. You must comply with the conditions in Section 3(a) if You Share 320 | all or a substantial portion of the contents of the database. 321 | 322 | For the avoidance of doubt, this Section 4 supplements and does not 323 | replace Your obligations under this Public License where the Licensed 324 | Rights include other Copyright and Similar Rights. 325 | 326 | 327 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 328 | 329 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 330 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 331 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 332 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 333 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 334 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 335 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 336 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 337 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 338 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 339 | 340 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 341 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 342 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 343 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 344 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 345 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 346 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 347 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 348 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 349 | 350 | c. The disclaimer of warranties and limitation of liability provided 351 | above shall be interpreted in a manner that, to the extent 352 | possible, most closely approximates an absolute disclaimer and 353 | waiver of all liability. 354 | 355 | 356 | Section 6 -- Term and Termination. 357 | 358 | a. This Public License applies for the term of the Copyright and 359 | Similar Rights licensed here. However, if You fail to comply with 360 | this Public License, then Your rights under this Public License 361 | terminate automatically. 362 | 363 | b. Where Your right to use the Licensed Material has terminated under 364 | Section 6(a), it reinstates: 365 | 366 | 1. automatically as of the date the violation is cured, provided 367 | it is cured within 30 days of Your discovery of the 368 | violation; or 369 | 370 | 2. upon express reinstatement by the Licensor. 371 | 372 | For the avoidance of doubt, this Section 6(b) does not affect any 373 | right the Licensor may have to seek remedies for Your violations 374 | of this Public License. 375 | 376 | c. For the avoidance of doubt, the Licensor may also offer the 377 | Licensed Material under separate terms or conditions or stop 378 | distributing the Licensed Material at any time; however, doing so 379 | will not terminate this Public License. 380 | 381 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 382 | License. 383 | 384 | 385 | Section 7 -- Other Terms and Conditions. 386 | 387 | a. The Licensor shall not be bound by any additional or different 388 | terms or conditions communicated by You unless expressly agreed. 389 | 390 | b. Any arrangements, understandings, or agreements regarding the 391 | Licensed Material not stated herein are separate from and 392 | independent of the terms and conditions of this Public License. 393 | 394 | 395 | Section 8 -- Interpretation. 396 | 397 | a. For the avoidance of doubt, this Public License does not, and 398 | shall not be interpreted to, reduce, limit, restrict, or impose 399 | conditions on any use of the Licensed Material that could lawfully 400 | be made without permission under this Public License. 401 | 402 | b. To the extent possible, if any provision of this Public License is 403 | deemed unenforceable, it shall be automatically reformed to the 404 | minimum extent necessary to make it enforceable. If the provision 405 | cannot be reformed, it shall be severed from this Public License 406 | without affecting the enforceability of the remaining terms and 407 | conditions. 408 | 409 | c. No term or condition of this Public License will be waived and no 410 | failure to comply consented to unless expressly agreed to by the 411 | Licensor. 412 | 413 | d. Nothing in this Public License constitutes or may be interpreted 414 | as a limitation upon, or waiver of, any privileges and immunities 415 | that apply to the Licensor or You, including from the legal 416 | processes of any jurisdiction or authority. 417 | 418 | ======================================================================= 419 | 420 | Creative Commons is not a party to its public 421 | licenses. Notwithstanding, Creative Commons may elect to apply one of 422 | its public licenses to material it publishes and in those instances 423 | will be considered the “Licensor.” The text of the Creative Commons 424 | public licenses is dedicated to the public domain under the CC0 Public 425 | Domain Dedication. Except for the limited purpose of indicating that 426 | material is shared under a Creative Commons public license or as 427 | otherwise permitted by the Creative Commons policies published at 428 | creativecommons.org/policies, Creative Commons does not authorize the 429 | use of the trademark "Creative Commons" or any other trademark or logo 430 | of Creative Commons without its prior written consent including, 431 | without limitation, in connection with any unauthorized modifications 432 | to any of its public licenses or any other arrangements, 433 | understandings, or agreements concerning use of licensed material. For 434 | the avoidance of doubt, this paragraph does not form part of the 435 | public licenses. 436 | 437 | Creative Commons may be contacted at creativecommons.org. 438 | --------------------------------------------------------------------------------